apadmi_grout 1.2.0 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (31) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +13 -0
  3. data/lib/apadmi/grout/actions/find_tickets_to_move_action/find_tickets_to_move_action.rb +24 -0
  4. data/lib/apadmi/grout/actions/find_tickets_to_move_action/find_tickets_to_move_ado_action.rb +91 -0
  5. data/lib/apadmi/grout/{jira/actions/find_tickets_to_move_action.rb → actions/find_tickets_to_move_action/find_tickets_to_move_jira_action.rb} +16 -17
  6. data/lib/apadmi/grout/actions/generate_release_notes_action/generate_release_notes_action.rb +48 -0
  7. data/lib/apadmi/grout/actions/generate_release_notes_action/issue_classifier.rb +51 -0
  8. data/lib/apadmi/grout/{release_notes/actions → actions}/issues_from_changelog_action.rb +11 -6
  9. data/lib/apadmi/grout/actions/move_tickets_action.rb +34 -0
  10. data/lib/apadmi/grout/di.rb +58 -9
  11. data/lib/apadmi/grout/{jira/models/find_tickets_options.rb → models/ado_config.rb} +2 -2
  12. data/lib/apadmi/grout/models/bitrise.rb +38 -0
  13. data/lib/apadmi/grout/models/find_tickets_options.rb +44 -0
  14. data/lib/apadmi/grout/{jira/models → models}/flag_messages.rb +0 -0
  15. data/lib/apadmi/grout/models/issue.rb +49 -0
  16. data/lib/apadmi/grout/{jira/models → models}/pull_request.rb +0 -0
  17. data/lib/apadmi/grout/models/release_notes_config.rb +47 -0
  18. data/lib/apadmi/grout/{release_notes/models → models}/release_notes_templates.rb +8 -8
  19. data/lib/apadmi/grout/service/bitrise_service/bitrise_service.rb +103 -0
  20. data/lib/apadmi/grout/service/board_service/ado_board_service.rb +199 -0
  21. data/lib/apadmi/grout/service/board_service/board_service.rb +59 -0
  22. data/lib/apadmi/grout/{jira/wrapper/jira_wrapper.rb → service/board_service/jira_board_service.rb} +65 -107
  23. data/lib/apadmi/grout/utils/git_utils.rb +32 -6
  24. data/lib/apadmi/grout/utils/network_service.rb +123 -0
  25. data/lib/apadmi/grout/version.rb +1 -1
  26. data/lib/apadmi_grout.rb +3 -21
  27. metadata +24 -15
  28. data/lib/apadmi/grout/jira/actions/move_jira_tickets_action.rb +0 -58
  29. data/lib/apadmi/grout/jira/models/version.rb +0 -23
  30. data/lib/apadmi/grout/release_notes/actions/generate_release_notes_action.rb +0 -39
  31. data/lib/apadmi/grout/release_notes/models/release_notes_config.rb +0 -74
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apadmi
4
+ module Grout
5
+ # @param tasks [Array<Apadmi::Grout::Issue>]
6
+ # @param features [Array<Apadmi::Grout::Issue>] aka stories
7
+ # @param improvements [Array<Apadmi::Grout::Issue>]
8
+ # @param defects [Array<Apadmi::Grout::Issue>] aka bugs
9
+ # @param others [Array<Apadmi::Grout::Issue>] any other non-standard issue types
10
+ ClassifiedIssues = Struct.new(
11
+ :tasks,
12
+ :features,
13
+ :improvements,
14
+ :defects,
15
+ :others
16
+ ) do
17
+ # @return returns true if all categories are empty
18
+ def empty
19
+ tasks.empty? && features.empty? &&
20
+ improvements.empty? && defects.empty? && others.empty?
21
+ end
22
+ end
23
+
24
+ # @param title [String] The title of the document
25
+ # @param app_version [String] The app version pertaining to this release
26
+ # @param min_os_version [String] The min supported os version of this release
27
+ # @param date [String] Today's date
28
+ # @param moved_issues [Array<Apadmi::Grout::Issue>] Issues moved to a new state by this build job
29
+ # @param release_issues [Array<Apadmi::Grout::Issue>] Issues considered part of this release
30
+ # @param commit_hash [String] Commit hash from which release was built
31
+ # @param ci_build_number [String] CI build number which built the release
32
+ # @param ci_build_url [String] Link to CI build job
33
+ # @param templates [Apadmi::Grout::Templates] Mustache templates to use to generate the document
34
+ ReleaseNotesConfig = Struct.new(
35
+ :title,
36
+ :app_version,
37
+ :min_os_version,
38
+ :date,
39
+ :moved_issues,
40
+ :release_issues,
41
+ :commit_hash,
42
+ :ci_build_number,
43
+ :ci_build_url,
44
+ :templates
45
+ )
46
+ end
47
+ end
@@ -9,7 +9,7 @@ module Apadmi
9
9
 
10
10
  ### Version
11
11
  {{config.app_version}}
12
- {{config.hello}}
12
+
13
13
  ### Release date
14
14
  {{config.date}}
15
15
 
@@ -20,9 +20,9 @@ module Apadmi
20
20
  Commit Hash: {{config.commit_hash}}
21
21
  [CI/CD build \#{{config.ci_build_number}}]({{config.ci_build_url}})
22
22
 
23
- ## Jira Tickets Moved By This Build
23
+ ## Tickets Moved By This Build
24
24
  {{ rendered_moved_issues }}
25
- ## Jira Sprint Release notes
25
+ ## Sprint Release notes
26
26
  {{ rendered_release_issues }} }
27
27
  end
28
28
 
@@ -30,35 +30,35 @@ Commit Hash: {{config.commit_hash}}
30
30
  %{### New functionality
31
31
  {{# classified_issues.features.first}}
32
32
  {{# classified_issues.features}}
33
- * [{{ key }} - {{ summary }}]({{ base_url }}{{ key }})
33
+ * [{{ key }} - {{ summary }}]({{ url }})
34
34
  {{/ classified_issues.features}}
35
35
 
36
36
  {{/ classified_issues.features.first}}
37
37
  {{# classified_issues.improvements.first}}
38
38
  ### Improvements
39
39
  {{# classified_issues.improvements}}
40
- * [{{ key }} - {{ summary }}]({{ base_url }}{{ key }})
40
+ * [{{ key }} - {{ summary }}]({{ url }})
41
41
  {{/ classified_issues.improvements}}
42
42
 
43
43
  {{/ classified_issues.improvements.first}}
44
44
  {{# classified_issues.tasks.first}}
45
45
  ### Completed Tasks
46
46
  {{# classified_issues.tasks}}
47
- * [{{ key }} - {{ summary }}]({{ base_url }}{{ key }})
47
+ * [{{ key }} - {{ summary }}]({{ url }})
48
48
  {{/ classified_issues.tasks}}
49
49
 
50
50
  {{/ classified_issues.tasks.first}}
51
51
  {{# classified_issues.defects.first}}
52
52
  ### Fixed defects
53
53
  {{# classified_issues.defects}}
54
- * [{{ key }} - {{ summary }}]({{ base_url }}{{ key }})
54
+ * [{{ key }} - {{ summary }}]({{ url }})
55
55
  {{/ classified_issues.defects}}
56
56
 
57
57
  {{/ classified_issues.defects.first}}
58
58
  {{# classified_issues.others.first}}
59
59
  ### Other issue types
60
60
  {{# classified_issues.others}}
61
- * [{{ key }} - {{ summary }}]({{ base_url }}{{ key }})
61
+ * [{{issue_type}} - {{ key }} - {{ summary }}]({{ url }})
62
62
  {{/ classified_issues.others}}
63
63
 
64
64
  {{/ classified_issues.others.first}}}
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open-uri"
4
+ require "fileutils"
5
+
6
+ module Apadmi
7
+ module Grout
8
+ # Utility class for abstracting a some of the bitrise APIs
9
+ class BitriseService
10
+ # @param [String] app_slug
11
+ # @param [Apadmi::Grout::NetworkService] network_service
12
+ def initialize(app_slug, network_service)
13
+ @app_slug = app_slug
14
+ @network_service = network_service
15
+ end
16
+
17
+ # @param [Integer] pull_request_id
18
+ # @param [String] workflow
19
+ # @return [Array<BitriseBuild>]
20
+ def list_builds(pull_request_id, workflow)
21
+ res = @network_service.do_get("/#{@app_slug}/builds?pull_request_id=#{pull_request_id}&workflow=#{workflow}")
22
+ parsed = JSON.parse(res.body)
23
+ items = parsed["data"]
24
+
25
+ return [] if items.nil? || items.empty?
26
+
27
+ items.map do |item|
28
+ BitriseBuild.new(
29
+ item["triggered_at"],
30
+ item["slug"],
31
+ item["status"],
32
+ item["commit_hash"]
33
+ )
34
+ end
35
+ end
36
+
37
+ # @param [String] build_slug
38
+ # @param [String] artifact_slug
39
+ # @return [Artifact]
40
+ def retrieve_artifact_metadata(build_slug, artifact_slug)
41
+ res = @network_service.do_get("/#{@app_slug}/builds/#{build_slug}/artifacts/#{artifact_slug}")
42
+ parsed = JSON.parse(res.body)
43
+
44
+ Artifact.new(parsed["data"]["title"], parsed["data"]["expiring_download_url"])
45
+ end
46
+
47
+ # @param [String] build_slug
48
+ # @param [String] output_dir
49
+ # Download all artifacts from a given build to a specified directory
50
+ def download_artifacts(build_slug, output_dir)
51
+ res = @network_service.do_get("/#{@app_slug}/builds/#{build_slug}/artifacts")
52
+ parsed = JSON.parse(res.body)
53
+ items = parsed["data"]
54
+
55
+ artifacts = items.map do |item|
56
+ retrieve_artifact_metadata(build_slug, item["slug"])
57
+ end
58
+
59
+ FileUtils.mkdir_p output_dir
60
+
61
+ artifacts.each do |item|
62
+ File.open("#{output_dir}/#{item.title}", "wb") do |file|
63
+ # rubocop:disable Security/Open
64
+ file.write URI.open(item.download_url).read
65
+ # rubocop:enable Security/Open
66
+ end
67
+ end
68
+ artifacts
69
+ end
70
+
71
+ # @param [String] workflow
72
+ # @param [String] branch
73
+ # @param [String] dest the destination branch of the pr
74
+ # @param [String] pull_request_id
75
+ # @param [String] pull_request_repo_url
76
+ # @param [String] commit_hash
77
+ def trigger_build(workflow, branch, dest, pull_request_id, pull_request_repo_url, commit_hash)
78
+ params = {
79
+ workflow_id: workflow,
80
+ branch: branch,
81
+ commit_hash: commit_hash
82
+ }
83
+
84
+ params[:branch_dest] = dest unless dest.strip.blank?
85
+ params[:pull_request_id] = pull_request_id unless pull_request_id.strip.blank?
86
+ params[:pull_request_repository_url] = pull_request_repo_url unless pull_request_repo_url.strip.blank?
87
+
88
+ payload = {
89
+ hook_info: {
90
+ type: "bitrise"
91
+ },
92
+ build_params: params,
93
+ triggered_by: "curl"
94
+ }
95
+
96
+ res = @network_service.do_post("/#{@app_slug}/builds", payload.to_json)
97
+ parsed = JSON.parse(res.body)
98
+
99
+ TriggeredBitriseBuild.from_json(parsed)
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,199 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "./board_service"
4
+
5
+ module Apadmi
6
+ module Grout
7
+ # Provides a layer of abstraction on top of the ADO api
8
+ class AdoBoardService < BoardService
9
+ API_VERSION_PARAM = "api-version=6.0"
10
+
11
+ # @param [Apadmi::Grout::NetworkService] network_service
12
+ # @param [Apadmi::Grout::AdoConfig] ado_config
13
+ # @param [Logger] logger
14
+ def initialize(network_service, ado_config, logger)
15
+ @network_service = network_service
16
+ @ado_config = ado_config
17
+ @logger = logger
18
+ end
19
+
20
+ # @param [String[]] keys
21
+ # @return [Array<Apadmi::Grout::Issue>]
22
+ def find_issues_by_keys(keys)
23
+ return [] if keys.length <= 0
24
+
25
+ keys = keys.map { |k| sanitise_key(k) } # Allows supporting raw keys, and keys prefixed with a hash which is common ADO practice
26
+
27
+ res = @network_service.do_get("/wit/workitems?ids=#{keys.join(",")}&api-version=7.1-preview.2")
28
+ parsed = JSON.parse(res.body)
29
+ items = parsed["value"]
30
+
31
+ return if items.nil? || items.empty?
32
+
33
+ items.map { |i| Apadmi::Grout::Issue.from_ado_hash(i) }
34
+ end
35
+
36
+ # @param component [String] Included to be consistent with JIRA, this will boil down to a tag in ADO
37
+ # @param status [String]
38
+ # @param ticket_types [String[]]
39
+ # @param options [Apadmi::Grout::AdoFindTicketsOptions] Additional options to filter by
40
+
41
+ # @return [Array<Apadmi::Grout::Issue>]
42
+ def search_unblocked_issues(component, status, ticket_types = [], options = nil)
43
+ tags = (options&.required_tags || []).push(*component).filter { |it| !it.blank? }
44
+ not_tags = (options&.not_tags || []).filter { |it| !it.blank? }
45
+
46
+ raise "Tags can either be required or not required, not both" unless (tags & not_tags).empty?
47
+
48
+ tags_filter = tags.map { |tag| " AND [System.Tags] CONTAINS '#{tag}' " }.join(" ")
49
+ not_tags_filter = not_tags.map { |tag| " AND [System.Tags] NOT CONTAINS '#{tag}'" }.join(" ")
50
+
51
+ status_filter = ("AND [System.State]='#{status}' " unless status.blank?) || ""
52
+ type_filter = ("AND [System.WorkItemType] IN (#{ticket_types.map { |t| "'#{t}'" }.join(", ")})" unless ticket_types.empty?) || ""
53
+ wiql = %(
54
+ Select [System.Id]
55
+ From WorkItems
56
+ Where [System.Id] >= 1
57
+ #{status_filter}
58
+ #{tags_filter}
59
+ #{not_tags_filter}
60
+ #{type_filter}
61
+ )
62
+
63
+ get_issues_by_wiql(wiql)
64
+ end
65
+
66
+ # @param [String] key
67
+ # @param [String] comment
68
+ def flag_ticket(key, comment)
69
+ key = sanitise_key(key)
70
+
71
+ add_tag(key, @ado_config.flag_tag)
72
+ add_comment(key, comment) unless comment.blank?
73
+ end
74
+
75
+ # @param [String] key
76
+ def un_flag_ticket(key)
77
+ key = sanitise_key(key)
78
+
79
+ remove_tag(key, @ado_config.flag_tag)
80
+ end
81
+
82
+ # @param [String] key
83
+ # @param [String] comment
84
+ def add_comment(key, comment)
85
+ key = sanitise_key(key)
86
+
87
+ payload = {
88
+ "text" => comment
89
+ }
90
+ @network_service.do_post("/wit/workitems/#{key}/comments?#{API_VERSION_PARAM}-preview.3", payload.to_json)
91
+ end
92
+
93
+ # @param [String] key
94
+ # @param [String] tag
95
+ def add_tag(key, tag)
96
+ payload = [{
97
+ "op" => "add",
98
+ "path" => "/fields/System.Tags",
99
+ "value" => tag
100
+ }]
101
+ @network_service.do_patch("/wit/workitems/#{key}?#{API_VERSION_PARAM}", payload.to_json)
102
+ end
103
+
104
+ # @param [String] key
105
+ # @param [String] tag
106
+ def remove_tag(key, tag)
107
+ current_tags = get_work_item(key)["fields"]["System.Tags"].split("; ")
108
+ new_tags = current_tags.filter { |t| t != tag }
109
+
110
+ payload = [{
111
+ "op" => "replace",
112
+ "path" => "/fields/System.Tags",
113
+ "value" => new_tags.join("; ")
114
+ }]
115
+ @network_service.do_patch("/wit/workitems/#{key}?#{API_VERSION_PARAM}", payload.to_json)
116
+ end
117
+
118
+ # @param [String] key
119
+ def flagged?(key)
120
+ current_tags = get_work_item(key)["fields"]["System.Tags"].split("; ")
121
+ current_tags.include?(@ado_config.flag_tag)
122
+ end
123
+
124
+ # @param [String] key
125
+ # @return [RawWorkItemHash]
126
+ def get_work_item(key)
127
+ key = sanitise_key(key)
128
+
129
+ res = @network_service.do_get("/wit/workitems/#{key}?#{API_VERSION_PARAM}")
130
+ JSON.parse(res.body)
131
+ end
132
+
133
+ # @param [String] key
134
+ # @param [Array<String>] versions
135
+ def assign_fixversions(key, versions)
136
+ key = sanitise_key(key)
137
+
138
+ payload = [{
139
+ "op" => "replace",
140
+ "path" => "/fields/Microsoft.VSTS.Build.IntegrationBuild",
141
+ "value" => versions.join("; ")
142
+ }]
143
+ @network_service.do_patch("/wit/workitems/#{key}?#{API_VERSION_PARAM}", payload.to_json)
144
+ end
145
+
146
+ # @param [String] key
147
+ # @return [Array<String>] fix versions
148
+ def get_ticket_fixversions(key)
149
+ key = sanitise_key(key)
150
+
151
+ get_work_item(key)["fields"]["Microsoft.VSTS.Build.IntegrationBuild"].split("; ")
152
+ end
153
+
154
+ # @param [Apadmi::Grout::Issue] issue
155
+ # @param [String] state_name
156
+ def transition_issue(issue, state_name)
157
+ key = sanitise_key(issue.key)
158
+
159
+ payload = [{
160
+ "op" => "add",
161
+ "path" => "/fields/System.State",
162
+ "value" => state_name
163
+ }]
164
+ @network_service.do_patch("/wit/workitems/#{key}?#{API_VERSION_PARAM}", payload.to_json)
165
+ end
166
+
167
+ # @param [String] key
168
+ # @return [String]
169
+ def get_ticket_status(key)
170
+ key = sanitise_key(key)
171
+
172
+ get_work_item(key)["fields"]["System.State"]
173
+ end
174
+
175
+ private
176
+
177
+ def sanitise_key(key)
178
+ key = key.to_s.strip.delete_prefix "#"
179
+ raise "Invalid key" if key.blank?
180
+
181
+ key
182
+ end
183
+
184
+ # @param wiql_query [String]
185
+ # @return [Array<Apadmi::Grout::Issue>]
186
+ def get_issues_by_wiql(wiql_query)
187
+ @logger.message("Executing wiql")
188
+ @logger.message(wiql_query)
189
+
190
+ payload = { "query" => wiql_query }
191
+ res = @network_service.do_post("/wit/wiql?#{API_VERSION_PARAM}", payload.to_json)
192
+ parsed = JSON.parse(res.body)
193
+ items = parsed["workItems"] || []
194
+ ids = items.map { |item| item["id"] } || []
195
+ find_issues_by_keys(ids)
196
+ end
197
+ end
198
+ end
199
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apadmi
4
+ module Grout
5
+ # A generic board service to be implemented by whatever
6
+ # board providers we have e.g. JIRA and ADO
7
+ class BoardService
8
+ # @param [String[]] keys
9
+ # @return [Array<Apadmi::Grout::Issue>]
10
+ def find_issues_by_keys(_keys)
11
+ raise "Unimplemented :("
12
+ end
13
+
14
+ # @param [String] _component
15
+ # @param [String] _status
16
+ # @param [String[]] _ticket_types
17
+ # @param [Apadmi::Grout::JiraFindTicketsOptions|Apadmi::Grout::JiraFindTicketsOptions] _options
18
+ # @return [Array<Apadmi::Grout::Issue>]
19
+ def search_unblocked_issues(_component, _status, _ticket_types = [], _options = nil)
20
+ raise "Unimplemented :("
21
+ end
22
+
23
+ # @param [String] _key
24
+ # @param [String] _comment
25
+ def flag_ticket(_key, _comment)
26
+ raise "Unimplemented :("
27
+ end
28
+
29
+ # @param [String] _key
30
+ def un_flag_ticket(_key)
31
+ raise "Unimplemented :("
32
+ end
33
+
34
+ # @param [String] _key
35
+ # @return bool
36
+ def flagged?(_key)
37
+ raise "Unimplemented :("
38
+ end
39
+
40
+ # @param [String] _key
41
+ # @param [Array<String>] _version_strings
42
+ def assign_fixversions(_key, _version_strings)
43
+ raise "Unimplemented :("
44
+ end
45
+
46
+ # @param [String] _key
47
+ # @return [Array<String>]
48
+ def get_ticket_fixversions(_key)
49
+ raise "Unimplemented :("
50
+ end
51
+
52
+ # @param [Apadmi::Grout::Issue] _issue
53
+ # @param [String] _state_name
54
+ def transition_issue(_issue, _state_name)
55
+ raise "Unimplemented :("
56
+ end
57
+ end
58
+ end
59
+ end