abt-cli 0.0.8 → 0.0.13

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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/bin/abt +0 -6
  3. data/lib/abt.rb +6 -0
  4. data/lib/abt/cli.rb +5 -5
  5. data/lib/abt/cli/dialogs.rb +36 -14
  6. data/lib/abt/git_config.rb +22 -5
  7. data/lib/abt/providers/asana/base_command.rb +11 -0
  8. data/lib/abt/providers/asana/commands/add.rb +75 -0
  9. data/lib/abt/providers/asana/commands/current.rb +3 -3
  10. data/lib/abt/providers/asana/commands/finalize.rb +1 -1
  11. data/lib/abt/providers/asana/commands/harvest_time_entry_data.rb +3 -4
  12. data/lib/abt/providers/asana/commands/init.rb +5 -0
  13. data/lib/abt/providers/asana/commands/pick.rb +17 -4
  14. data/lib/abt/providers/asana/commands/share.rb +3 -3
  15. data/lib/abt/providers/asana/commands/start.rb +1 -1
  16. data/lib/abt/providers/asana/commands/tasks.rb +2 -0
  17. data/lib/abt/providers/asana/configuration.rb +9 -3
  18. data/lib/abt/providers/devops.rb +19 -0
  19. data/lib/abt/providers/devops/api.rb +95 -0
  20. data/lib/abt/providers/devops/base_command.rb +98 -0
  21. data/lib/abt/providers/devops/commands/boards.rb +34 -0
  22. data/lib/abt/providers/devops/commands/clear.rb +24 -0
  23. data/lib/abt/providers/devops/commands/clear_global.rb +24 -0
  24. data/lib/abt/providers/devops/commands/current.rb +93 -0
  25. data/lib/abt/providers/devops/commands/harvest_time_entry_data.rb +53 -0
  26. data/lib/abt/providers/devops/commands/init.rb +72 -0
  27. data/lib/abt/providers/devops/commands/pick.rb +78 -0
  28. data/lib/abt/providers/devops/commands/share.rb +26 -0
  29. data/lib/abt/providers/devops/commands/work-items.rb +46 -0
  30. data/lib/abt/providers/devops/configuration.rb +110 -0
  31. data/lib/abt/providers/harvest/base_command.rb +11 -0
  32. data/lib/abt/providers/harvest/commands/current.rb +3 -3
  33. data/lib/abt/providers/harvest/commands/pick.rb +1 -0
  34. data/lib/abt/providers/harvest/commands/start.rb +10 -11
  35. data/lib/abt/providers/harvest/commands/tasks.rb +2 -0
  36. data/lib/abt/providers/harvest/commands/track.rb +6 -4
  37. data/lib/abt/providers/harvest/configuration.rb +4 -3
  38. data/lib/abt/version.rb +1 -1
  39. metadata +16 -2
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ Dir.glob("#{File.expand_path(__dir__)}/devops/*.rb").sort.each { |file| require file }
4
+ Dir.glob("#{File.expand_path(__dir__)}/devops/commands/*.rb").sort.each { |file| require file }
5
+
6
+ module Abt
7
+ module Providers
8
+ module Devops
9
+ def self.command_names
10
+ Commands.constants.sort.map { |constant_name| Helpers.const_to_command(constant_name) }
11
+ end
12
+
13
+ def self.command_class(name)
14
+ const_name = Helpers.command_to_const(name)
15
+ Commands.const_get(const_name) if Commands.const_defined?(const_name)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Providers
5
+ module Devops
6
+ class Api
7
+ VERBS = %i[get post put].freeze
8
+
9
+ CONDITIONAL_ACCESS_POLICY_ERROR_CODE = 'VS403463'
10
+
11
+ attr_reader :organization_name, :project_name, :username, :access_token, :cli
12
+
13
+ def initialize(organization_name:, project_name:, username:, access_token:, cli:)
14
+ @organization_name = organization_name
15
+ @project_name = project_name
16
+ @username = username
17
+ @access_token = access_token
18
+ @cli = cli
19
+ end
20
+
21
+ VERBS.each do |verb|
22
+ define_method(verb) do |*args|
23
+ request(verb, *args)
24
+ end
25
+ end
26
+
27
+ def get_paged(path, query = {})
28
+ result = request(:get, path, query)
29
+ result['value']
30
+
31
+ # TODO: Loop if necessary
32
+ end
33
+
34
+ def work_item_query(wiql)
35
+ response = post('wit/wiql', Oj.dump({ query: wiql }, mode: :json))
36
+ ids = response['workItems'].map { |work_item| work_item['id'] }
37
+
38
+ work_items = []
39
+ ids.each_slice(200) do |page_ids|
40
+ work_items += get_paged('wit/workitems', ids: page_ids.join(','))
41
+ end
42
+
43
+ work_items
44
+ end
45
+
46
+ def request(*args)
47
+ response = connection.public_send(*args)
48
+
49
+ if response.success?
50
+ Oj.load(response.body)
51
+ else
52
+ error_class = Abt::HttpError.error_class_for_status(response.status)
53
+ encoded_response_body = response.body.force_encoding('utf-8')
54
+ raise error_class, "Code: #{response.status}, body: #{encoded_response_body}"
55
+ end
56
+ rescue Abt::HttpError::ForbiddenError => e
57
+ handle_denied_by_conditional_access_policy!(e)
58
+ end
59
+
60
+ def base_url
61
+ "https://#{organization_name}.visualstudio.com/#{project_name}"
62
+ end
63
+
64
+ def api_endpoint
65
+ "#{base_url}/_apis"
66
+ end
67
+
68
+ def url_for_work_item(work_item)
69
+ "#{base_url}/_workitems/edit/#{work_item['id']}"
70
+ end
71
+
72
+ def connection
73
+ @connection ||= Faraday.new(api_endpoint) do |connection|
74
+ connection.basic_auth username, access_token
75
+ connection.headers['Content-Type'] = 'application/json'
76
+ connection.headers['Accept'] = 'application/json; api-version=6.0'
77
+ end
78
+ end
79
+
80
+ private
81
+
82
+ def handle_denied_by_conditional_access_policy!(exception)
83
+ raise exception unless exception.message.include?(CONDITIONAL_ACCESS_POLICY_ERROR_CODE)
84
+
85
+ cli.abort <<~TXT
86
+ Access denied by conditional access policy.
87
+ Try logging into the board using the URL below, then retry the command.
88
+
89
+ #{base_url}
90
+ TXT
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Providers
5
+ module Devops
6
+ class BaseCommand
7
+ attr_reader :arg_str, :organization_name, :project_name, :board_id, :work_item_id, :cli, :config
8
+
9
+ def initialize(arg_str:, cli:)
10
+ @arg_str = arg_str
11
+
12
+ @config = Configuration.new(cli: cli)
13
+ @cli = cli
14
+
15
+ if arg_str.nil?
16
+ use_current_args
17
+ else
18
+ use_arg_str(arg_str)
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def require_board!
25
+ return if organization_name && project_name && board_id
26
+
27
+ cli.abort 'No current/specified board. Did you initialize DevOps?'
28
+ end
29
+
30
+ def require_work_item!
31
+ unless organization_name && project_name && board_id
32
+ cli.abort 'No current/specified board. Did you initialize DevOps and pick a work item?'
33
+ end
34
+
35
+ return if work_item_id
36
+
37
+ cli.abort 'No current/specified work item. Did you pick a DevOps work item?'
38
+ end
39
+
40
+ def sanitize_work_item(work_item)
41
+ return nil if work_item.nil?
42
+
43
+ work_item.merge(
44
+ 'id' => work_item['id'].to_s,
45
+ 'name' => work_item['fields']['System.Title'],
46
+ 'url' => api.url_for_work_item(work_item)
47
+ )
48
+ end
49
+
50
+ def same_args_as_config?
51
+ organization_name == config.organization_name &&
52
+ project_name == config.project_name &&
53
+ board_id == config.board_id &&
54
+ work_item_id == config.work_item_id
55
+ end
56
+
57
+ def print_board(organization_name, project_name, board)
58
+ arg_str = "#{organization_name}/#{project_name}/#{board['id']}"
59
+
60
+ cli.print_provider_command('devops', arg_str, board['name'])
61
+ # cli.warn board['url'] if board.key?('url') && cli.output.isatty # TODO: Web URL
62
+ end
63
+
64
+ def print_work_item(organization, project, board, work_item)
65
+ arg_str = "#{organization}/#{project}/#{board['id']}/#{work_item['id']}"
66
+
67
+ cli.print_provider_command('devops', arg_str, work_item['name'])
68
+ cli.warn work_item['url'] if work_item.key?('url') && cli.output.isatty
69
+ end
70
+
71
+ def use_current_args
72
+ @organization_name = config.organization_name
73
+ @project_name = config.project_name
74
+ @board_id = config.board_id
75
+ @work_item_id = config.work_item_id
76
+ end
77
+
78
+ def use_arg_str(arg_str)
79
+ args = arg_str.to_s.split('/')
80
+
81
+ if args.length < 3
82
+ cli.abort 'Argument format is <organization>/<project>/<board-id>[/<work-item-id>]'
83
+ end
84
+
85
+ (@organization_name, @project_name, @board_id, @work_item_id) = args
86
+ end
87
+
88
+ def api
89
+ Abt::Providers::Devops::Api.new(organization_name: organization_name,
90
+ project_name: project_name,
91
+ username: config.username_for_organization(organization_name),
92
+ access_token: config.access_token_for_organization(organization_name),
93
+ cli: cli)
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Providers
5
+ module Devops
6
+ module Commands
7
+ class Boards < BaseCommand
8
+ def self.command
9
+ 'boards devops'
10
+ end
11
+
12
+ def self.description
13
+ 'List all boards - useful for piping into grep etc'
14
+ end
15
+
16
+ def call
17
+ cli.abort 'No organization selected. Did you initialize DevOps?' if organization_name.nil?
18
+ cli.abort 'No project selected. Did you initialize DevOps?' if project_name.nil?
19
+
20
+ boards.map do |board|
21
+ print_board(organization_name, project_name, board)
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def boards
28
+ @boards ||= api.get_paged('work/boards')
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Providers
5
+ module Devops
6
+ module Commands
7
+ class Clear < BaseCommand
8
+ def self.command
9
+ 'clear devops'
10
+ end
11
+
12
+ def self.description
13
+ 'Clear DevOps config for current git repository'
14
+ end
15
+
16
+ def call
17
+ cli.warn 'Clearing configuration'
18
+ config.clear_local
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Providers
5
+ module Devops
6
+ module Commands
7
+ class ClearGlobal < BaseCommand
8
+ def self.command
9
+ 'clear-global devops'
10
+ end
11
+
12
+ def self.description
13
+ 'Clear all global configuration'
14
+ end
15
+
16
+ def call
17
+ cli.warn 'Clearing global DevOps configuration'
18
+ config.clear_global
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Providers
5
+ module Devops
6
+ module Commands
7
+ class Current < BaseCommand
8
+ def self.command
9
+ 'current devops[:<organization-name>/<project-name>/<board-id>[/<work-item-id>]]'
10
+ end
11
+
12
+ def self.description
13
+ 'Get or set DevOps configuration for current git repository'
14
+ end
15
+
16
+ def call
17
+ require_board!
18
+
19
+ if same_args_as_config? || !config.local_available?
20
+ show_current_configuration
21
+ else
22
+ cli.warn 'Updating configuration'
23
+ update_configuration
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def show_current_configuration
30
+ if work_item_id.nil?
31
+ print_board(organization_name, project_name, board)
32
+ else
33
+ print_work_item(organization_name, project_name, board, work_item)
34
+ end
35
+ end
36
+
37
+ def update_configuration
38
+ ensure_board_is_valid!
39
+
40
+ if work_item_id.nil?
41
+ update_board_config
42
+ config.work_item_id = nil
43
+
44
+ print_board(organization_name, project_name, board)
45
+ else
46
+ ensure_work_item_is_valid!
47
+
48
+ update_board_config
49
+ config.work_item_id = work_item_id
50
+
51
+ print_work_item(organization_name, project_name, board, work_item)
52
+ end
53
+ end
54
+
55
+ def update_board_config
56
+ config.organization_name = organization_name
57
+ config.project_name = project_name
58
+ config.board_id = board_id
59
+ end
60
+
61
+ def ensure_board_is_valid!
62
+ if board.nil?
63
+ cli.abort 'Board could not be found, ensure that settings for organization, project, and board are correct'
64
+ end
65
+ end
66
+
67
+ def ensure_work_item_is_valid!
68
+ cli.abort "No such work item: ##{work_item_id}" if work_item.nil?
69
+ end
70
+
71
+ def board
72
+ @board ||= begin
73
+ cli.warn 'Fetching board...'
74
+ api.get("work/boards/#{board_id}")
75
+ rescue HttpError::NotFoundError
76
+ nil
77
+ end
78
+ end
79
+
80
+ def work_item
81
+ @work_item ||= begin
82
+ cli.warn 'Fetching work item...'
83
+ work_item = api.get_paged('wit/workitems', ids: work_item_id)[0]
84
+ sanitize_work_item(work_item)
85
+ rescue HttpError::NotFoundError
86
+ nil
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Providers
5
+ module Devops
6
+ module Commands
7
+ class HarvestTimeEntryData < BaseCommand
8
+ def self.command
9
+ 'harvest-time-entry-data devops[:<organization-name>/<project-name>/<board-id>/<work-item-id>]'
10
+ end
11
+
12
+ def self.description
13
+ 'Print Harvest time entry data for DevOps work item as json. Used by harvest start script.'
14
+ end
15
+
16
+ def call
17
+ require_work_item!
18
+
19
+ body = {
20
+ notes: notes,
21
+ external_reference: {
22
+ id: work_item['id'],
23
+ group_id: 'AzureDevOpsWorkItem',
24
+ permalink: work_item['url']
25
+ }
26
+ }
27
+
28
+ cli.puts Oj.dump(body, mode: :json)
29
+ end
30
+
31
+ private
32
+
33
+ def notes
34
+ [
35
+ 'Azure DevOps',
36
+ work_item['fields']['System.WorkItemType'],
37
+ "##{work_item['id']}",
38
+ '-',
39
+ work_item['name']
40
+ ].join(' ')
41
+ end
42
+
43
+ def work_item
44
+ @work_item ||= begin
45
+ work_item = api.get_paged('wit/workitems', ids: work_item_id)[0]
46
+ sanitize_work_item(work_item)
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end