geordi 12.4.0 → 12.6.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/CHANGELOG.md +14 -0
- data/Gemfile.lock +1 -1
- data/README.md +13 -8
- data/exe/dumple +18 -6
- data/lib/geordi/commands/branch.rb +16 -2
- data/lib/geordi/commands/commit.rb +17 -2
- data/lib/geordi/commands/deploy.rb +35 -6
- data/lib/geordi/commands/dump.rb +8 -5
- data/lib/geordi/commands/security_update.rb +3 -1
- data/lib/geordi/git.rb +53 -0
- data/lib/geordi/{gitlinear.rb → linear_client.rb} +118 -56
- data/lib/geordi/remote.rb +3 -4
- data/lib/geordi/settings.rb +44 -1
- data/lib/geordi/util.rb +12 -24
- data/lib/geordi/version.rb +1 -1
- metadata +7 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2f8359f51e8fa9d619b9d84ede59104e711d59e33c2f034e6a3bc1a8933fd457
|
4
|
+
data.tar.gz: 2d234986282ecc98a4853a8d6b4f8103556ca10694596b399b534aab3b8ce3d5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8415640931dfa6738300ab40b3a4ace0d232993cf7b29aacc924f3b9de3a13b82b45b9064984dcc263883ec50bcc8717e97671140c8c45413842a94566e6ff13
|
7
|
+
data.tar.gz: c4f0ed717cd6dadfc770019444b35e47fc8176ff65971a99fef0e71eeb37d2e740201b000738db5e12bdf8d3043e957f77a05d92165fc2562fde9de6ea8239ec
|
data/CHANGELOG.md
CHANGED
@@ -10,6 +10,20 @@ This project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html
|
|
10
10
|
### Breaking changes
|
11
11
|
|
12
12
|
|
13
|
+
## 12.6.0 2025-09-22
|
14
|
+
|
15
|
+
### Compatible changes
|
16
|
+
|
17
|
+
- `geordi dump`: Allow to forward the compression option to the underlying `dumple` command, e.g. `geordi dump --compress=zstd:3` (for PostgreSQL) or `geordi dump --compress` (for MySQL).
|
18
|
+
- `dumple`: Allow to specify a compression algorithm for PostgreSQL, e.g. `dumple --compress=zstd:3`. The already supported compression for MySQL `dumple --compress` is kept untouched.
|
19
|
+
|
20
|
+
|
21
|
+
## 12.5.0 2025-09-09
|
22
|
+
|
23
|
+
### Compatible changes
|
24
|
+
* `geordi deploy` will now offer to move deployed issues to a new state if linear_team_ids are configured.
|
25
|
+
|
26
|
+
|
13
27
|
## 12.4.0 2025-08-29
|
14
28
|
|
15
29
|
### Compatible changes
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -65,8 +65,8 @@ Example: `geordi chromedriver_update`
|
|
65
65
|
This command will find and install the matching chromedriver for the currently
|
66
66
|
installed Chrome.
|
67
67
|
|
68
|
-
Setting `auto_update_chromedriver` to `true` in your global Geordi config file
|
69
|
-
(`~/.config/geordi/global.yml`), will automatically update chromedriver before
|
68
|
+
Setting `auto_update_chromedriver` to `true` in your global Geordi config file
|
69
|
+
(`~/.config/geordi/global.yml`), will automatically update chromedriver before
|
70
70
|
cucumber tests if a newer chromedriver version is available.
|
71
71
|
|
72
72
|
|
@@ -98,8 +98,8 @@ servers. When passed a number, directly connects to the selected server.
|
|
98
98
|
IRB flags can be given as `irb_flags: '...'` in the global or local Geordi config file
|
99
99
|
(`~/.config/geordi/global.yml` / `./.geordi.yml`). If you define irb_flags in both files, the local config file will be
|
100
100
|
used. For IRB >=1.2 in combination with Ruby <3 geordi automatically sets the `--nomultiline` flag, to prevent slow
|
101
|
-
pasting. You can override this behavior by setting `--multiline` in the global config file or by defining `irb_flags`
|
102
|
-
in the local config file. The latter will always turn off the automatic behavior, even if you don't set any values for
|
101
|
+
pasting. You can override this behavior by setting `--multiline` in the global config file or by defining `irb_flags`
|
102
|
+
in the local config file. The latter will always turn off the automatic behavior, even if you don't set any values for
|
103
103
|
the irb_flags key.
|
104
104
|
|
105
105
|
**Options**
|
@@ -170,6 +170,8 @@ Finds available Capistrano stages by their prefix, e.g. `geordi deploy p` will
|
|
170
170
|
deploy production, `geordi deploy mak` will deploy a `makandra` stage if there
|
171
171
|
is a file config/deploy/makandra.rb.
|
172
172
|
|
173
|
+
If Linear team ids are configured (see `geordi commit`), will offer to move deployed issues to a new state. Disable with "skip".
|
174
|
+
|
173
175
|
When your project is running Capistrano 3, deployment will use `cap deploy`
|
174
176
|
instead of `cap deploy:migrations`. You can force using `deploy` by passing the
|
175
177
|
-M option: `geordi deploy -M staging`.
|
@@ -189,8 +191,8 @@ and offer to delete them. Excluded are databases that are whitelisted. This come
|
|
189
191
|
in handy when you're keeping your currently active projects in the whitelist files
|
190
192
|
and perform regular housekeeping with Geordi.
|
191
193
|
|
192
|
-
Per default, Geordi will try to connect to the databases as a local user without
|
193
|
-
password authorization.
|
194
|
+
Per default, Geordi will try to connect to the databases as a local user without
|
195
|
+
password authorization.
|
194
196
|
|
195
197
|
Geordi will ask for confirmation before actually dropping databases and will
|
196
198
|
offer to edit the whitelist instead.
|
@@ -232,6 +234,7 @@ not match, please issue separate commands for dumping (`dump -d`) and sourcing
|
|
232
234
|
**Options**
|
233
235
|
- `-l, --load=[DUMP_FILE]`: Load a dump
|
234
236
|
- `-d, --database=NAME`: Target database, if there are multiple databases
|
237
|
+
- `-c, --compress=[ALGORITHM]`: Compress the dump file (default for PSQL)
|
235
238
|
|
236
239
|
|
237
240
|
### `geordi help [COMMAND]`
|
@@ -339,7 +342,7 @@ Run all employed tests.
|
|
339
342
|
When running `geordi tests` without any arguments, all unit tests, rspec specs
|
340
343
|
and cucumber features will be run.
|
341
344
|
|
342
|
-
When passing file paths or directories as arguments, Geordi will forward them to `rspec` and `cucumber`.
|
345
|
+
When passing file paths or directories as arguments, Geordi will forward them to `rspec` and `cucumber`.
|
343
346
|
All rspec specs and cucumber features matching the given paths will be run.
|
344
347
|
|
345
348
|
|
@@ -387,7 +390,9 @@ Stores a timestamped database dump for the given Rails environment in `~/dumps`:
|
|
387
390
|
|
388
391
|
**Options**
|
389
392
|
- `-i`: Print disk usage of `~/dumps`
|
390
|
-
- `--
|
393
|
+
- `--fail-gently`: On error, do not crash but print a warning and exit(0)
|
394
|
+
- `--for-download`: Dump to `~/dumps/dump_for_download.dump`
|
395
|
+
- `--compress`: Compress the dump (default for PostgreSQL) and optionally set the compression algorithm (only available for PostgreSQL)
|
391
396
|
|
392
397
|
|
393
398
|
Contributing
|
data/exe/dumple
CHANGED
@@ -50,7 +50,7 @@ def cd_to_project_root(fail_gently)
|
|
50
50
|
end
|
51
51
|
end
|
52
52
|
|
53
|
-
def dump_command(dump_file, config)
|
53
|
+
def dump_command(dump_file, config, compress)
|
54
54
|
host = config['host']
|
55
55
|
port = config['port']
|
56
56
|
|
@@ -79,6 +79,7 @@ def dump_command(dump_file, config)
|
|
79
79
|
command << " pg_dump #{config['database']}"
|
80
80
|
command << " --clean"
|
81
81
|
command << " --format=custom"
|
82
|
+
command << " --compress=#{compress}" if compress.is_a?(String)
|
82
83
|
command << " --file=#{dump_file}"
|
83
84
|
command << " --username=\"#{config['username']}\""
|
84
85
|
command << " --host=#{host}" if host
|
@@ -124,7 +125,7 @@ def prepare_dump_path(config)
|
|
124
125
|
run "chmod 700 #{DUMPS_DIR}"
|
125
126
|
end
|
126
127
|
|
127
|
-
if ARGV.include?
|
128
|
+
if ARGV.include?('--for_download') || ARGV.include?('--for-download')
|
128
129
|
"#{DUMPS_DIR}/dump_for_download.dump"
|
129
130
|
else
|
130
131
|
"#{DUMPS_DIR}/#{config['database']}_#{Time.now.strftime("%Y%m%d_%H%M%S")}.dump"
|
@@ -133,8 +134,14 @@ end
|
|
133
134
|
|
134
135
|
begin
|
135
136
|
fail_gently = ARGV.include?("--fail-gently")
|
136
|
-
compress = ARGV.
|
137
|
-
|
137
|
+
compress = ARGV.find do |argument|
|
138
|
+
if argument == '--compress'
|
139
|
+
break true
|
140
|
+
elsif argument.start_with?('--compress=')
|
141
|
+
break argument.split('=').last
|
142
|
+
end
|
143
|
+
end
|
144
|
+
environment, database = ARGV.reject { |argument| argument.start_with?('-') }
|
138
145
|
|
139
146
|
cd_to_project_root(fail_gently)
|
140
147
|
config = find_database_config(DB_CONFIG_PATH, environment, database)
|
@@ -142,12 +149,17 @@ begin
|
|
142
149
|
|
143
150
|
# Dump!
|
144
151
|
given_database = database ? %(#{database} ) : ""
|
145
|
-
command = dump_command(dump_path, config)
|
152
|
+
command = dump_command(dump_path, config, compress)
|
146
153
|
puts "> Dumping #{given_database}database for \"#{environment}\" environment ..."
|
147
154
|
run command or raise "x Creating the dump failed."
|
148
155
|
run "chmod 600 #{dump_path}"
|
149
156
|
|
150
|
-
if compress
|
157
|
+
if config['adapter'] == 'mysql' && compress.is_a?(String)
|
158
|
+
puts "> Cannot compress a MySQL dump with #{compress}, falling back to gzip."
|
159
|
+
end
|
160
|
+
|
161
|
+
# For PostgreSQL, #dump_command will do the compression. MySQL needs manual compression.
|
162
|
+
if config['adapter'] == 'mysql' && compress
|
151
163
|
puts "> Compressing the dump ..."
|
152
164
|
# gzip compresses in place
|
153
165
|
compress_success = run "gzip #{dump_path}"
|
@@ -9,8 +9,22 @@ LONGDESC
|
|
9
9
|
option :from_master, aliases: %w[-m --from-main], type: :boolean, desc: 'Branch from master instead of the current branch'
|
10
10
|
|
11
11
|
def branch
|
12
|
-
require 'geordi/
|
13
|
-
|
12
|
+
require 'geordi/linear_client'
|
13
|
+
require 'geordi/git'
|
14
|
+
|
15
|
+
issue = LinearClient.new.choose_issue
|
16
|
+
|
17
|
+
local_branches = Git.local_branch_names
|
18
|
+
matching_local_branch = local_branches.find { |branch_name| branch_name == issue['branchName'] }
|
19
|
+
matching_local_branch ||= local_branches.find { |branch_name| branch_name.include? issue['identifier'].to_s }
|
20
|
+
|
21
|
+
if matching_local_branch
|
22
|
+
Util.run! ['git', 'checkout', matching_local_branch]
|
23
|
+
else
|
24
|
+
default_branch = Git.default_branch
|
25
|
+
Util.run! ['git', 'checkout', default_branch] if options.from_master
|
26
|
+
Util.run! ['git', 'checkout', '-b', issue['branchName']]
|
27
|
+
end
|
14
28
|
|
15
29
|
Hint.did_you_know [
|
16
30
|
:commit,
|
@@ -9,8 +9,23 @@ stored in `~/.config/geordi/global.yml`.
|
|
9
9
|
LONGDESC
|
10
10
|
|
11
11
|
def commit(*git_args)
|
12
|
-
require 'geordi/
|
13
|
-
|
12
|
+
require 'geordi/linear_client'
|
13
|
+
require 'geordi/git'
|
14
|
+
require 'highline'
|
15
|
+
|
16
|
+
Interaction.warn <<~WARNING unless Git.staged_changes?
|
17
|
+
No staged changes. Will create an empty commit.
|
18
|
+
WARNING
|
19
|
+
|
20
|
+
linear_client = LinearClient.new
|
21
|
+
highline = HighLine.new
|
22
|
+
|
23
|
+
issue = linear_client.issue_from_branch || linear_client.choose_issue
|
24
|
+
title = "[#{issue['identifier']}] #{issue['title']}"
|
25
|
+
description = "Issue: #{issue['url']}"
|
26
|
+
extra = highline.ask("\nAdd an optional message").strip
|
27
|
+
title << ' - ' << extra if extra != ''
|
28
|
+
Util.run!(['git', 'commit', '--allow-empty', '-m', title, '-m', description, *git_args])
|
14
29
|
|
15
30
|
Hint.did_you_know [
|
16
31
|
:branch,
|
@@ -28,6 +28,8 @@ Finds available Capistrano stages by their prefix, e.g. `geordi deploy p` will
|
|
28
28
|
deploy production, `geordi deploy mak` will deploy a `makandra` stage if there
|
29
29
|
is a file config/deploy/makandra.rb.
|
30
30
|
|
31
|
+
If Linear team ids are configured (see `geordi commit`), will offer to move deployed issues to a new state. Disable with "skip".
|
32
|
+
|
31
33
|
When your project is running Capistrano 3, deployment will use `cap deploy`
|
32
34
|
instead of `cap deploy:migrations`. You can force using `deploy` by passing the
|
33
35
|
-M option: `geordi deploy -M staging`.
|
@@ -39,6 +41,12 @@ option :current_branch, aliases: '-c', type: :boolean,
|
|
39
41
|
desc: 'Set DEPLOY_BRANCH to the current branch during deploy'
|
40
42
|
|
41
43
|
def deploy(target_stage = nil)
|
44
|
+
require 'geordi/git'
|
45
|
+
require 'geordi/linear_client'
|
46
|
+
|
47
|
+
settings = Settings.new
|
48
|
+
linear_client = LinearClient.new
|
49
|
+
|
42
50
|
# Set/Infer default values
|
43
51
|
branch_stage_map = { 'master' => 'staging', 'main' => 'staging', 'production' => 'production' }
|
44
52
|
if target_stage && !Util.deploy_targets.include?(target_stage)
|
@@ -48,7 +56,7 @@ def deploy(target_stage = nil)
|
|
48
56
|
end
|
49
57
|
|
50
58
|
# Ask for required information
|
51
|
-
target_stage ||= Interaction.prompt 'Deployment stage:', branch_stage_map.fetch(
|
59
|
+
target_stage ||= Interaction.prompt 'Deployment stage:', branch_stage_map.fetch(Git.current_branch, 'staging')
|
52
60
|
capistrano_config = CapistranoConfig.new(target_stage)
|
53
61
|
|
54
62
|
if options.current_branch
|
@@ -60,18 +68,25 @@ def deploy(target_stage = nil)
|
|
60
68
|
set :branch, ENV['DEPLOY_BRANCH'] || 'master'
|
61
69
|
ERROR
|
62
70
|
|
63
|
-
source_branch = target_branch =
|
71
|
+
source_branch = target_branch = Git.current_branch
|
64
72
|
else # Normal deploy
|
65
|
-
source_branch = Interaction.prompt 'Source branch:',
|
73
|
+
source_branch = Interaction.prompt 'Source branch:', Git.current_branch
|
66
74
|
|
67
75
|
deploy_branch = capistrano_config.branch
|
68
|
-
deploy_branch ||=
|
76
|
+
deploy_branch ||= Git.default_branch
|
69
77
|
target_branch = Interaction.prompt 'Deploy branch:', deploy_branch
|
70
78
|
end
|
71
79
|
|
80
|
+
if settings.linear_integration_set_up?
|
81
|
+
config_state = settings.linear_state_after_deploy(target_stage)
|
82
|
+
config_state = 'skip' if config_state.empty?
|
83
|
+
target_state = Interaction.prompt("Move deployed Linear issues to state:", config_state)
|
84
|
+
target_state = '' if target_state.empty? || target_state == 'skip'
|
85
|
+
settings.persist_linear_state_after_deploy(target_stage, target_state)
|
86
|
+
end
|
87
|
+
|
72
88
|
merge_needed = (source_branch != target_branch)
|
73
89
|
push_needed = merge_needed || `git cherry -v | wc -l`.strip.to_i > 0
|
74
|
-
push_needed = false if Util.testing? # Hard to test
|
75
90
|
|
76
91
|
Interaction.announce "Checking whether your #{source_branch} branch is ready" ############
|
77
92
|
Util.run!("git checkout #{source_branch}")
|
@@ -89,13 +104,23 @@ def deploy(target_stage = nil)
|
|
89
104
|
|
90
105
|
Interaction.announce 'You are about to:' #################################################
|
91
106
|
Interaction.note "Merge branch #{source_branch} into #{target_branch}" if merge_needed
|
107
|
+
linear_issue_ids = []
|
92
108
|
if push_needed
|
93
|
-
Interaction.note 'Push these commits:'
|
109
|
+
Interaction.note 'Push these commits:'
|
94
110
|
Util.run!("git --no-pager log origin/#{target_branch}..#{source_branch} --oneline")
|
111
|
+
|
112
|
+
commit_messages = Git.commits_between(source_branch, target_branch)
|
113
|
+
linear_issue_ids = linear_client.extract_issue_ids(commit_messages)
|
95
114
|
end
|
96
115
|
Interaction.note "Deploy to #{target_stage}"
|
97
116
|
Interaction.note "From current branch #{source_branch}" if options.current_branch
|
98
117
|
|
118
|
+
if !linear_issue_ids.empty? && target_state && !target_state.empty?
|
119
|
+
relevant_commits = linear_client.filter_by_issue_ids(commit_messages, linear_issue_ids)
|
120
|
+
Interaction.note("Move these Linear issues to state \"#{target_state}\":")
|
121
|
+
puts relevant_commits.join("\n")
|
122
|
+
end
|
123
|
+
|
99
124
|
if Interaction.prompt('Go ahead with the deployment?', 'n', /y|yes/)
|
100
125
|
puts
|
101
126
|
git_call = []
|
@@ -115,6 +140,10 @@ def deploy(target_stage = nil)
|
|
115
140
|
|
116
141
|
Util.run!(capistrano_call, show_cmd: true)
|
117
142
|
|
143
|
+
if !linear_issue_ids.empty? && target_state && !target_state.empty?
|
144
|
+
linear_client.move_issues_to_state(linear_issue_ids, target_state)
|
145
|
+
end
|
146
|
+
|
118
147
|
Interaction.success 'Deployment complete.'
|
119
148
|
|
120
149
|
Hint.did_you_know [
|
data/lib/geordi/commands/dump.rb
CHANGED
@@ -26,11 +26,11 @@ DESC
|
|
26
26
|
|
27
27
|
option :load, aliases: '-l', type: :string, desc: 'Load a dump', banner: '[DUMP_FILE]'
|
28
28
|
option :database, aliases: '-d', type: :string, desc: 'Target database, if there are multiple databases', banner: 'NAME'
|
29
|
+
option :compress, aliases: '-c', type: :string, desc: 'Compress the dump file (default for PSQL)', banner: '[ALGORITHM]'
|
29
30
|
|
30
31
|
def dump(target = nil, *_args)
|
31
32
|
require 'geordi/dump_loader'
|
32
33
|
require 'geordi/remote'
|
33
|
-
database = options[:database] ? "#{options[:database]} " : ''
|
34
34
|
|
35
35
|
if target.nil? # Local …
|
36
36
|
if options.load # … dump loading
|
@@ -46,14 +46,17 @@ def dump(target = nil, *_args)
|
|
46
46
|
|
47
47
|
else # … dump creation
|
48
48
|
Interaction.announce 'Dumping the development database'
|
49
|
-
Util.run!(
|
49
|
+
Util.run!(Util.dumple_command('development', options))
|
50
|
+
|
51
|
+
database = "#{options[:database]} " if options[:database]
|
50
52
|
Interaction.success "Successfully dumped the #{database}development database."
|
51
53
|
end
|
52
54
|
|
53
55
|
else # Remote dumping …
|
54
|
-
database_label =
|
56
|
+
database_label = target.dup
|
57
|
+
database_label << " (#{options[:database]} database)" if options[:database]
|
55
58
|
|
56
|
-
Interaction.announce "Dumping the database of #{
|
59
|
+
Interaction.announce "Dumping the database of #{database_label}"
|
57
60
|
dump_path = Geordi::Remote.new(target).dump(options)
|
58
61
|
|
59
62
|
if options.load # … and dump loading
|
@@ -65,7 +68,7 @@ def dump(target = nil, *_args)
|
|
65
68
|
Util.run! "rm #{dump_path}"
|
66
69
|
Interaction.note "Dump file removed"
|
67
70
|
|
68
|
-
Interaction.success "Your #{loader.config['database']} database has now the data of #{
|
71
|
+
Interaction.success "Your #{loader.config['database']} database has now the data of #{database_label}."
|
69
72
|
end
|
70
73
|
end
|
71
74
|
|
data/lib/geordi/git.rb
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
module Geordi
|
2
|
+
class Git
|
3
|
+
class << self
|
4
|
+
def local_branch_names
|
5
|
+
@local_branch_names ||= begin
|
6
|
+
branch_list_string = if Util.testing?
|
7
|
+
ENV['GEORDI_TESTING_GIT_BRANCHES'].to_s
|
8
|
+
else
|
9
|
+
`git branch --format="%(refname:short)"`
|
10
|
+
end
|
11
|
+
|
12
|
+
branch_list_string.strip.split("\n")
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def current_branch
|
17
|
+
if Util.testing?
|
18
|
+
default_branch
|
19
|
+
else
|
20
|
+
`git rev-parse --abbrev-ref HEAD`.strip
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def staged_changes?
|
25
|
+
if Util.testing?
|
26
|
+
ENV['GEORDI_TESTING_STAGED_CHANGES'] == 'true'
|
27
|
+
else
|
28
|
+
statuses = `git status --porcelain`.split("\n")
|
29
|
+
statuses.any? { |l| /^[A-Z]/i =~ l }
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def default_branch
|
34
|
+
default_branch = if Util.testing?
|
35
|
+
ENV['GEORDI_TESTING_DEFAULT_BRANCH']
|
36
|
+
else
|
37
|
+
head_symref = `git ls-remote --symref origin HEAD`
|
38
|
+
head_symref[%r{\Aref: refs/heads/(\S+)\sHEAD}, 1]
|
39
|
+
end
|
40
|
+
|
41
|
+
default_branch || 'master'
|
42
|
+
end
|
43
|
+
|
44
|
+
def commits_between(source_branch, target_branch)
|
45
|
+
return [ENV['GEORDI_TESTING_GIT_COMMIT']] if Util.testing?
|
46
|
+
|
47
|
+
commits = `git --no-pager log --pretty=format:%s origin/#{target_branch}..#{source_branch}`
|
48
|
+
|
49
|
+
commits&.split("\n")
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -4,7 +4,7 @@ require 'net/http'
|
|
4
4
|
require 'json'
|
5
5
|
|
6
6
|
module Geordi
|
7
|
-
class
|
7
|
+
class LinearClient
|
8
8
|
# This require-style is to prevent Ruby from loading files of a different
|
9
9
|
# version of Geordi.
|
10
10
|
require File.expand_path('settings', __dir__)
|
@@ -16,47 +16,6 @@ module Geordi
|
|
16
16
|
self.settings = Settings.new
|
17
17
|
end
|
18
18
|
|
19
|
-
def commit(git_args)
|
20
|
-
Interaction.warn <<~WARNING unless Util.staged_changes?
|
21
|
-
No staged changes. Will create an empty commit.
|
22
|
-
WARNING
|
23
|
-
|
24
|
-
issue = issue_from_branch || choose_issue
|
25
|
-
create_commit "[#{issue['identifier']}] #{issue['title']}", "Issue: #{issue['url']}", *git_args
|
26
|
-
end
|
27
|
-
|
28
|
-
def branch(from_master: false)
|
29
|
-
issue = choose_issue
|
30
|
-
|
31
|
-
local_branches = local_branch_names
|
32
|
-
matching_local_branch = local_branches.find { |branch_name| branch_name == issue['branchName'] }
|
33
|
-
matching_local_branch ||= local_branches.find { |branch_name| branch_name.include? issue['identifier'].to_s }
|
34
|
-
|
35
|
-
if matching_local_branch
|
36
|
-
Util.run! ['git', 'checkout', matching_local_branch]
|
37
|
-
else
|
38
|
-
default_branch = Util.git_default_branch
|
39
|
-
Util.run! ['git', 'checkout', default_branch] if from_master
|
40
|
-
Util.run! ['git', 'checkout', '-b', issue['branchName']]
|
41
|
-
end
|
42
|
-
end
|
43
|
-
|
44
|
-
private
|
45
|
-
|
46
|
-
attr_accessor :highline, :settings
|
47
|
-
|
48
|
-
def local_branch_names
|
49
|
-
@local_branch_names ||= begin
|
50
|
-
branch_list_string = if Util.testing?
|
51
|
-
ENV['GEORDI_TESTING_GIT_BRANCHES'].to_s
|
52
|
-
else
|
53
|
-
`git branch --format="%(refname:short)"`
|
54
|
-
end
|
55
|
-
|
56
|
-
branch_list_string.strip.split("\n")
|
57
|
-
end
|
58
|
-
end
|
59
|
-
|
60
19
|
def choose_issue
|
61
20
|
if Util.testing?
|
62
21
|
return dummy_issue_for_testing
|
@@ -94,23 +53,64 @@ module Geordi
|
|
94
53
|
nil
|
95
54
|
end
|
96
55
|
|
56
|
+
def move_issues_to_state(issue_identifiers, state)
|
57
|
+
return if Util.testing?
|
58
|
+
|
59
|
+
issues = fetch_linear_issues # This only retrieves issues for the configured linear team ids
|
60
|
+
state_ids_by_team_id = state_ids_by_team_id(state)
|
61
|
+
|
62
|
+
issue_identifiers.each do |identifier|
|
63
|
+
issue = issues.find { |i| i['identifier'] == identifier }
|
64
|
+
|
65
|
+
skip unless issue && (state_id = state_ids_by_team_id[issue.dig('team', 'id')])
|
66
|
+
|
67
|
+
update_issue_state(issue['id'], state_id)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
97
71
|
def issue_from_branch
|
98
72
|
issue = if Util.testing?
|
99
73
|
dummy_issue_for_testing if ENV['GEORDI_TESTING_ISSUE_MATCHES'] == 'true'
|
100
74
|
else
|
101
|
-
current_branch =
|
102
|
-
|
75
|
+
current_branch = Git.current_branch
|
76
|
+
fetch_linear_issues.find { |issue| issue['branchName'] == current_branch }
|
103
77
|
end
|
104
78
|
|
105
|
-
|
79
|
+
if issue
|
80
|
+
id = issue['identifier']
|
81
|
+
title = issue['title']
|
82
|
+
|
83
|
+
Interaction.note 'Auto-detected issue from branch name:'
|
84
|
+
puts HighLine::BOLD + "[#{id}] #{title}" + HighLine::RESET
|
85
|
+
|
86
|
+
issue if Interaction.prompt('Use it?', 'y', /y|yes/i)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def extract_issue_ids(commit_messages)
|
91
|
+
found_ids = []
|
92
|
+
|
93
|
+
regex = /^\[[A-Z]+\d*-\d+\]/
|
94
|
+
|
95
|
+
commit_messages&.each do |line|
|
96
|
+
line&.scan(regex) do |match|
|
97
|
+
found_ids << match
|
98
|
+
end
|
99
|
+
end
|
106
100
|
|
107
|
-
id
|
108
|
-
|
101
|
+
found_ids.map { |id| id.delete('[]') } # [W-365] => W-365
|
102
|
+
end
|
109
103
|
|
110
|
-
|
111
|
-
|
104
|
+
def filter_by_issue_ids(list_of_strings, issue_ids)
|
105
|
+
list_of_strings.select do |message|
|
106
|
+
issue_ids.any? { |id| message.start_with?("[#{id}]") }
|
107
|
+
end
|
112
108
|
end
|
113
109
|
|
110
|
+
private
|
111
|
+
|
112
|
+
attr_accessor :highline, :settings
|
113
|
+
|
114
114
|
def dummy_issue_for_testing
|
115
115
|
settings.linear_api_key
|
116
116
|
ENV['GEORDI_TESTING_NO_LINEAR_ISSUES'] == 'true' ? Geordi::Interaction.fail('No issues to offer.') : {
|
@@ -123,12 +123,6 @@ module Geordi
|
|
123
123
|
}
|
124
124
|
end
|
125
125
|
|
126
|
-
def create_commit(title, description, *git_args)
|
127
|
-
extra = highline.ask("\nAdd an optional message").strip
|
128
|
-
title << ' - ' << extra if extra != ''
|
129
|
-
Util.run!(['git', 'commit', '--allow-empty', '-m', title, '-m', description, *git_args])
|
130
|
-
end
|
131
|
-
|
132
126
|
def fetch_linear_issues
|
133
127
|
@linear_issues ||= begin
|
134
128
|
team_ids = settings.linear_team_ids
|
@@ -150,6 +144,7 @@ module Geordi
|
|
150
144
|
nodes {
|
151
145
|
title
|
152
146
|
identifier
|
147
|
+
id
|
153
148
|
url
|
154
149
|
branchName
|
155
150
|
assignee {
|
@@ -159,7 +154,10 @@ module Geordi
|
|
159
154
|
state {
|
160
155
|
name
|
161
156
|
position
|
162
|
-
|
157
|
+
}
|
158
|
+
team {
|
159
|
+
id
|
160
|
+
}
|
163
161
|
}
|
164
162
|
}
|
165
163
|
}
|
@@ -169,6 +167,70 @@ module Geordi
|
|
169
167
|
end
|
170
168
|
end
|
171
169
|
|
170
|
+
def state_ids_by_team_id(state_name)
|
171
|
+
result = {}
|
172
|
+
|
173
|
+
team_ids = settings.linear_team_ids
|
174
|
+
filter = {
|
175
|
+
"team": {
|
176
|
+
"id": {
|
177
|
+
"in": team_ids,
|
178
|
+
}
|
179
|
+
}
|
180
|
+
}
|
181
|
+
response = query_api(<<~GRAPHQL, filter: filter)
|
182
|
+
query workflowStates($filter: WorkflowStateFilter) {
|
183
|
+
workflowStates(filter: $filter) {
|
184
|
+
nodes {
|
185
|
+
id
|
186
|
+
name
|
187
|
+
team {
|
188
|
+
id
|
189
|
+
name
|
190
|
+
}
|
191
|
+
}
|
192
|
+
}
|
193
|
+
}
|
194
|
+
GRAPHQL
|
195
|
+
|
196
|
+
response = response.dig(*%w[workflowStates nodes])
|
197
|
+
|
198
|
+
team_ids.each do |team_id|
|
199
|
+
found_state = response.find do |item|
|
200
|
+
item["team"]["id"] == team_id && item["name"] == state_name
|
201
|
+
end
|
202
|
+
|
203
|
+
if found_state
|
204
|
+
result[team_id] = found_state["id"]
|
205
|
+
else
|
206
|
+
team_identifier = response.find { |item| item.dig('team', 'id') == team_id }&.dig('team', 'name') || team_id
|
207
|
+
Interaction.warn("Could not find the state \"#{state_name}\" for team \"#{team_identifier}\". Skipping its issues.")
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
if result.empty?
|
212
|
+
Interaction.fail("The issue state #{state_name.inspect} does not exist.")
|
213
|
+
end
|
214
|
+
|
215
|
+
result
|
216
|
+
end
|
217
|
+
|
218
|
+
def update_issue_state(issue_id, state_id)
|
219
|
+
query_api(<<~GRAPHQL, nil)
|
220
|
+
mutation UpdateIssueState {
|
221
|
+
issueUpdate(
|
222
|
+
id: "#{issue_id}"
|
223
|
+
input: {
|
224
|
+
stateId: "#{state_id}"
|
225
|
+
}
|
226
|
+
)
|
227
|
+
{
|
228
|
+
success
|
229
|
+
}
|
230
|
+
}
|
231
|
+
GRAPHQL
|
232
|
+
end
|
233
|
+
|
172
234
|
def query_api(attributes, variables)
|
173
235
|
uri = URI(API_ENDPOINT)
|
174
236
|
loading_message = "Connecting to #{uri.host} ... "
|
data/lib/geordi/remote.rb
CHANGED
@@ -33,11 +33,9 @@ module Geordi
|
|
33
33
|
end
|
34
34
|
|
35
35
|
def dump(options = {})
|
36
|
-
database = options[:database] ? " #{options[:database]}" : ''
|
37
36
|
# Generate dump on the server
|
38
|
-
|
39
|
-
|
40
|
-
})
|
37
|
+
dumple = Util.dumple_command(@config.env, options.merge(for_download: true))
|
38
|
+
shell(options.merge(remote_command: dumple))
|
41
39
|
|
42
40
|
destination_directory = File.join(@config.root, 'tmp')
|
43
41
|
FileUtils.mkdir_p destination_directory
|
@@ -48,6 +46,7 @@ module Geordi
|
|
48
46
|
server = @config.primary_server
|
49
47
|
Util.run!("scp -C #{@config.user(server)}@#{server}:#{REMOTE_DUMP_PATH} #{destination_path}")
|
50
48
|
|
49
|
+
database = " #{options[:database]}" if options[:database]
|
51
50
|
Interaction.success "Dumped the#{database} #{@stage} database to #{relative_destination}."
|
52
51
|
|
53
52
|
destination_path
|
data/lib/geordi/settings.rb
CHANGED
@@ -16,7 +16,7 @@ module Geordi
|
|
16
16
|
linear_team_ids
|
17
17
|
].freeze
|
18
18
|
|
19
|
-
ALLOWED_LOCAL_SETTINGS = %w[ linear_team_ids irb_flags
|
19
|
+
ALLOWED_LOCAL_SETTINGS = %w[ linear_team_ids linear_state_after_deploy irb_flags].freeze
|
20
20
|
|
21
21
|
SETTINGS_WARNED = 'GEORDI_INVALID_SETTINGS_WARNED'
|
22
22
|
|
@@ -75,6 +75,32 @@ module Geordi
|
|
75
75
|
team_ids
|
76
76
|
end
|
77
77
|
|
78
|
+
def linear_integration_set_up?
|
79
|
+
team_ids = get_linear_team_ids
|
80
|
+
!team_ids.empty?
|
81
|
+
end
|
82
|
+
|
83
|
+
def linear_state_after_deploy(stage)
|
84
|
+
config_state = @local_settings['linear_state_after_deploy']
|
85
|
+
|
86
|
+
if config_state && config_state[stage]
|
87
|
+
config_state[stage]
|
88
|
+
else
|
89
|
+
''
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def persist_linear_state_after_deploy(stage, target_state)
|
94
|
+
config_state = @local_settings.dig('linear_state_after_deploy', stage)
|
95
|
+
|
96
|
+
unless target_state.eql?(config_state)
|
97
|
+
@local_settings['linear_state_after_deploy'] ||= Hash.new
|
98
|
+
@local_settings['linear_state_after_deploy'][stage] = target_state
|
99
|
+
save_local_settings
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
|
78
104
|
private
|
79
105
|
|
80
106
|
def read_settings
|
@@ -125,6 +151,16 @@ module Geordi
|
|
125
151
|
end
|
126
152
|
end
|
127
153
|
|
154
|
+
def save_local_settings
|
155
|
+
unless Util.testing?
|
156
|
+
local_path = LOCAL_SETTINGS_FILE_NAME
|
157
|
+
|
158
|
+
File.open(local_path, 'w') do |file|
|
159
|
+
file.write @local_settings.to_yaml
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
128
164
|
def inquire_linear_api_key
|
129
165
|
Geordi::Interaction.note 'Create a personal API key here: https://linear.app/makandra/settings/account/security'
|
130
166
|
token = Geordi::Interaction.prompt("Please enter the API key:")
|
@@ -135,6 +171,13 @@ module Geordi
|
|
135
171
|
token
|
136
172
|
end
|
137
173
|
|
174
|
+
def get_linear_team_ids
|
175
|
+
local_team_ids = normalize_team_ids(@local_settings['linear_team_ids'])
|
176
|
+
global_team_ids = normalize_team_ids(@global_settings['linear_team_ids'])
|
177
|
+
|
178
|
+
local_team_ids | global_team_ids
|
179
|
+
end
|
180
|
+
|
138
181
|
def normalize_team_ids(team_ids)
|
139
182
|
case team_ids
|
140
183
|
when Array
|
data/lib/geordi/util.rb
CHANGED
@@ -119,21 +119,20 @@ module Geordi
|
|
119
119
|
end
|
120
120
|
end
|
121
121
|
|
122
|
-
def
|
123
|
-
if
|
124
|
-
|
125
|
-
|
126
|
-
|
122
|
+
def dumple_command(environment, options)
|
123
|
+
compress = if options[:compress] == 'compress'
|
124
|
+
'--compress'
|
125
|
+
elsif options[:compress]
|
126
|
+
"--compress=#{options[:compress]}"
|
127
127
|
end
|
128
|
-
end
|
129
128
|
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
129
|
+
cmd = ['dumple']
|
130
|
+
cmd << environment
|
131
|
+
cmd << options[:database]
|
132
|
+
cmd << compress
|
133
|
+
cmd << '--for-download' if options[:for_download]
|
134
|
+
|
135
|
+
cmd.compact.join(' ')
|
137
136
|
end
|
138
137
|
|
139
138
|
def deploy_targets
|
@@ -222,17 +221,6 @@ module Geordi
|
|
222
221
|
%r{(^|\/)spec|_spec\.rb($|:)}.match?(path)
|
223
222
|
end
|
224
223
|
|
225
|
-
def git_default_branch
|
226
|
-
default_branch = if testing?
|
227
|
-
ENV['GEORDI_TESTING_DEFAULT_BRANCH']
|
228
|
-
else
|
229
|
-
head_symref = `git ls-remote --symref origin HEAD`
|
230
|
-
head_symref[%r{\Aref: refs/heads/(\S+)\sHEAD}, 1]
|
231
|
-
end
|
232
|
-
|
233
|
-
default_branch || 'master'
|
234
|
-
end
|
235
|
-
|
236
224
|
end
|
237
225
|
end
|
238
226
|
end
|
data/lib/geordi/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: geordi
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 12.
|
4
|
+
version: 12.6.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Henning Koch
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-
|
11
|
+
date: 2025-09-22 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: thor
|
@@ -102,9 +102,10 @@ files:
|
|
102
102
|
- lib/geordi/cucumber.rb
|
103
103
|
- lib/geordi/db_cleaner.rb
|
104
104
|
- lib/geordi/dump_loader.rb
|
105
|
-
- lib/geordi/
|
105
|
+
- lib/geordi/git.rb
|
106
106
|
- lib/geordi/hint.rb
|
107
107
|
- lib/geordi/interaction.rb
|
108
|
+
- lib/geordi/linear_client.rb
|
108
109
|
- lib/geordi/remote.rb
|
109
110
|
- lib/geordi/settings.rb
|
110
111
|
- lib/geordi/util.rb
|
@@ -123,7 +124,7 @@ metadata:
|
|
123
124
|
bug_tracker_uri: https://github.com/makandra/geordi/issues
|
124
125
|
changelog_uri: https://github.com/makandra/geordi/blob/master/CHANGELOG.md
|
125
126
|
rubygems_mfa_required: 'true'
|
126
|
-
post_install_message:
|
127
|
+
post_install_message:
|
127
128
|
rdoc_options: []
|
128
129
|
require_paths:
|
129
130
|
- lib
|
@@ -139,7 +140,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
139
140
|
version: '0'
|
140
141
|
requirements: []
|
141
142
|
rubygems_version: 3.1.6
|
142
|
-
signing_key:
|
143
|
+
signing_key:
|
143
144
|
specification_version: 4
|
144
145
|
summary: Collection of command line tools we use in our daily work with Ruby, Rails
|
145
146
|
and Linux at makandra.
|