plan_my_stuff 0.6.0 → 0.8.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 +41 -1
- data/README.md +100 -103
- data/app/controllers/plan_my_stuff/application_controller.rb +22 -3
- data/app/controllers/plan_my_stuff/comments_controller.rb +14 -16
- data/app/controllers/plan_my_stuff/issues/approvals_controller.rb +23 -13
- data/app/controllers/plan_my_stuff/issues/closures_controller.rb +7 -5
- data/app/controllers/plan_my_stuff/issues/links_controller.rb +14 -18
- data/app/controllers/plan_my_stuff/issues/takes_controller.rb +99 -28
- data/app/controllers/plan_my_stuff/issues/viewers_controller.rb +13 -5
- data/app/controllers/plan_my_stuff/issues/waitings_controller.rb +7 -5
- data/app/controllers/plan_my_stuff/issues_controller.rb +24 -28
- data/app/controllers/plan_my_stuff/labels_controller.rb +21 -5
- data/app/controllers/plan_my_stuff/project_items/assignments_controller.rb +13 -6
- data/app/controllers/plan_my_stuff/project_items/statuses_controller.rb +5 -4
- data/app/controllers/plan_my_stuff/project_items_controller.rb +30 -5
- data/app/controllers/plan_my_stuff/projects_controller.rb +16 -16
- data/app/controllers/plan_my_stuff/testing_project_items/results_controller.rb +21 -11
- data/app/controllers/plan_my_stuff/testing_project_items_controller.rb +9 -4
- data/app/controllers/plan_my_stuff/testing_projects_controller.rb +30 -14
- data/app/controllers/plan_my_stuff/webhooks/aws_controller.rb +50 -17
- data/app/controllers/plan_my_stuff/webhooks/github_controller.rb +32 -49
- data/app/jobs/plan_my_stuff/application_job.rb +2 -3
- data/app/jobs/plan_my_stuff/reminders_sweep_job.rb +15 -22
- data/app/views/plan_my_stuff/comments/edit.html.erb +1 -3
- data/app/views/plan_my_stuff/comments/partials/_form.html.erb +1 -0
- data/app/views/plan_my_stuff/issues/edit.html.erb +2 -4
- data/app/views/plan_my_stuff/issues/index.html.erb +2 -2
- data/app/views/plan_my_stuff/issues/new.html.erb +2 -4
- data/app/views/plan_my_stuff/issues/partials/_approvals.html.erb +23 -2
- data/app/views/plan_my_stuff/issues/partials/_form.html.erb +1 -0
- data/app/views/plan_my_stuff/issues/partials/_labels.html.erb +2 -1
- data/app/views/plan_my_stuff/issues/partials/_links.html.erb +50 -7
- data/app/views/plan_my_stuff/issues/partials/_viewers.html.erb +2 -1
- data/app/views/plan_my_stuff/issues/show.html.erb +5 -2
- data/app/views/plan_my_stuff/partials/_flash.html.erb +4 -0
- data/app/views/plan_my_stuff/projects/edit.html.erb +1 -3
- data/app/views/plan_my_stuff/projects/index.html.erb +1 -1
- data/app/views/plan_my_stuff/projects/new.html.erb +1 -3
- data/app/views/plan_my_stuff/projects/partials/_form.html.erb +1 -0
- data/app/views/plan_my_stuff/projects/show.html.erb +13 -3
- data/app/views/plan_my_stuff/testing_project_items/new.html.erb +1 -3
- data/app/views/plan_my_stuff/testing_project_items/results/new.html.erb +1 -3
- data/app/views/plan_my_stuff/testing_projects/edit.html.erb +1 -3
- data/app/views/plan_my_stuff/testing_projects/new.html.erb +1 -3
- data/app/views/plan_my_stuff/testing_projects/partials/_form.html.erb +4 -3
- data/app/views/plan_my_stuff/testing_projects/partials/_item.html.erb +1 -0
- data/app/views/plan_my_stuff/testing_projects/partials/items/_form.html.erb +1 -0
- data/app/views/plan_my_stuff/testing_projects/show.html.erb +2 -2
- data/config/routes.rb +2 -2
- data/lib/generators/plan_my_stuff/install/templates/initializer.rb +56 -3
- data/lib/plan_my_stuff/approval.rb +12 -4
- data/lib/plan_my_stuff/aws_sns_simulator.rb +12 -6
- data/lib/plan_my_stuff/base_metadata.rb +4 -15
- data/lib/plan_my_stuff/base_project.rb +68 -55
- data/lib/plan_my_stuff/base_project_item.rb +61 -57
- data/lib/plan_my_stuff/base_project_metadata.rb +1 -1
- data/lib/plan_my_stuff/client.rb +136 -48
- data/lib/plan_my_stuff/comment.rb +57 -57
- data/lib/plan_my_stuff/comment_metadata.rb +1 -1
- data/lib/plan_my_stuff/configuration.rb +95 -82
- data/lib/plan_my_stuff/errors.rb +10 -10
- data/lib/plan_my_stuff/graphql/queries.rb +1 -1
- data/lib/plan_my_stuff/issue.rb +501 -322
- data/lib/plan_my_stuff/issue_metadata.rb +10 -10
- data/lib/plan_my_stuff/label.rb +32 -16
- data/lib/plan_my_stuff/link.rb +15 -15
- data/lib/plan_my_stuff/markdown.rb +12 -6
- data/lib/plan_my_stuff/metadata_parser.rb +3 -1
- data/lib/plan_my_stuff/notifications.rb +1 -1
- data/lib/plan_my_stuff/pipeline/completed_sweep.rb +2 -2
- data/lib/plan_my_stuff/pipeline/issue_linker.rb +1 -1
- data/lib/plan_my_stuff/pipeline.rb +61 -83
- data/lib/plan_my_stuff/project.rb +4 -4
- data/lib/plan_my_stuff/project_item_metadata.rb +1 -1
- data/lib/plan_my_stuff/project_metadata.rb +1 -1
- data/lib/plan_my_stuff/reminders/closer.rb +1 -1
- data/lib/plan_my_stuff/reminders/fire.rb +3 -3
- data/lib/plan_my_stuff/reminders/sweep.rb +4 -4
- data/lib/plan_my_stuff/repo.rb +12 -6
- data/lib/plan_my_stuff/test_helpers.rb +11 -11
- data/lib/plan_my_stuff/testing_project.rb +12 -11
- data/lib/plan_my_stuff/testing_project_item.rb +11 -9
- data/lib/plan_my_stuff/testing_project_metadata.rb +2 -2
- data/lib/plan_my_stuff/version.rb +1 -1
- data/lib/plan_my_stuff/webhook_replayer.rb +14 -2
- data/lib/plan_my_stuff.rb +26 -2
- data/lib/tasks/plan_my_stuff.rake +33 -20
- metadata +3 -2
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module PlanMyStuff
|
|
4
|
-
class IssueMetadata < BaseMetadata
|
|
4
|
+
class IssueMetadata < PlanMyStuff::BaseMetadata
|
|
5
5
|
# @return [Time, nil] first support action timestamp, nil until set
|
|
6
6
|
attr_accessor :responded_at
|
|
7
7
|
# @return [String, nil] user-facing URL in the consuming app
|
|
@@ -123,7 +123,7 @@ module PlanMyStuff
|
|
|
123
123
|
#
|
|
124
124
|
def normalize_links(raw)
|
|
125
125
|
Array.wrap(raw).filter_map do |entry|
|
|
126
|
-
PlanMyStuff::Link.build(entry)
|
|
126
|
+
PlanMyStuff::Link.build!(entry)
|
|
127
127
|
rescue ActiveModel::ValidationError, ArgumentError
|
|
128
128
|
next
|
|
129
129
|
end
|
|
@@ -209,16 +209,16 @@ module PlanMyStuff
|
|
|
209
209
|
def visible_to?(user)
|
|
210
210
|
return true if public?
|
|
211
211
|
|
|
212
|
-
resolved = UserResolver.resolve(user)
|
|
213
|
-
return true if UserResolver.support?(resolved)
|
|
212
|
+
resolved = PlanMyStuff::UserResolver.resolve(user)
|
|
213
|
+
return true if PlanMyStuff::UserResolver.support?(resolved)
|
|
214
214
|
|
|
215
|
-
visibility_allowlist.include?(UserResolver.user_id(resolved))
|
|
215
|
+
visibility_allowlist.include?(PlanMyStuff::UserResolver.user_id(resolved))
|
|
216
216
|
end
|
|
217
217
|
|
|
218
218
|
# @return [Hash]
|
|
219
219
|
def to_h
|
|
220
220
|
super.merge(
|
|
221
|
-
responded_at: format_time(responded_at),
|
|
221
|
+
responded_at: PlanMyStuff.format_time(responded_at),
|
|
222
222
|
issues_url: issues_url,
|
|
223
223
|
priority_list: priority_list,
|
|
224
224
|
priority_list_priority: priority_list_priority,
|
|
@@ -227,12 +227,12 @@ module PlanMyStuff
|
|
|
227
227
|
auto_complete: auto_complete,
|
|
228
228
|
links: links.map(&:to_h),
|
|
229
229
|
approvals: approvals.map(&:to_h),
|
|
230
|
-
waiting_on_user_at: format_time(waiting_on_user_at),
|
|
231
|
-
waiting_on_approval_at: format_time(waiting_on_approval_at),
|
|
232
|
-
next_reminder_at: format_time(next_reminder_at),
|
|
230
|
+
waiting_on_user_at: PlanMyStuff.format_time(waiting_on_user_at),
|
|
231
|
+
waiting_on_approval_at: PlanMyStuff.format_time(waiting_on_approval_at),
|
|
232
|
+
next_reminder_at: PlanMyStuff.format_time(next_reminder_at),
|
|
233
233
|
reminder_days: reminder_days,
|
|
234
234
|
closed_by_inactivity: closed_by_inactivity,
|
|
235
|
-
archived_at: format_time(archived_at),
|
|
235
|
+
archived_at: PlanMyStuff.format_time(archived_at),
|
|
236
236
|
)
|
|
237
237
|
end
|
|
238
238
|
end
|
data/lib/plan_my_stuff/label.rb
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module PlanMyStuff
|
|
4
|
-
# Wraps a GitHub label with a reference to its parent issue.
|
|
5
|
-
#
|
|
4
|
+
# Wraps a GitHub label with a reference to its parent issue. Class methods provide the public API for add/remove
|
|
5
|
+
# operations.
|
|
6
6
|
class Label < PlanMyStuff::ApplicationRecord
|
|
7
7
|
# @return [String, nil]
|
|
8
8
|
attribute :name, :string
|
|
@@ -18,14 +18,14 @@ module PlanMyStuff
|
|
|
18
18
|
#
|
|
19
19
|
# @return [Array<PlanMyStuff::Label>]
|
|
20
20
|
#
|
|
21
|
-
def add(issue:, labels:, user: nil)
|
|
21
|
+
def add!(issue:, labels:, user: nil)
|
|
22
22
|
label_names = Array.wrap(labels)
|
|
23
23
|
|
|
24
24
|
result = PlanMyStuff.client.rest(
|
|
25
25
|
:add_labels_to_an_issue, issue.repo, issue.number, label_names,
|
|
26
26
|
)
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
PlanMyStuff::Cache.delete_issue(issue.repo, issue.number)
|
|
29
29
|
|
|
30
30
|
PlanMyStuff::Notifications.instrument(
|
|
31
31
|
'label.added', issue, user: user, labels: label_names,
|
|
@@ -34,9 +34,8 @@ module PlanMyStuff
|
|
|
34
34
|
result.map { |gh_label| build(gh_label, issue: issue) }
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
-
# Ensures a label exists on the given repo, creating it if missing.
|
|
38
|
-
#
|
|
39
|
-
# +add_label+ (concurrent-creation race) is treated as success.
|
|
37
|
+
# Ensures a label exists on the given repo, creating it if missing. Idempotent: a 404 from +label+ triggers
|
|
38
|
+
# creation; a 422 from +add_label+ (concurrent-creation race) is treated as success.
|
|
40
39
|
#
|
|
41
40
|
# @param repo [String, Symbol] repo name or key
|
|
42
41
|
# @param name [String] label name
|
|
@@ -51,7 +50,25 @@ module PlanMyStuff
|
|
|
51
50
|
rescue PlanMyStuff::APIError => e
|
|
52
51
|
raise unless e.status == 404
|
|
53
52
|
|
|
54
|
-
create_label(client, repo, name, color, description)
|
|
53
|
+
create_label!(client, repo, name, color, description)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Returns whether a label exists in the repo.
|
|
57
|
+
#
|
|
58
|
+
# @raise [PlanMyStuff::APIError] if the GET fails with any status other than 404
|
|
59
|
+
#
|
|
60
|
+
# @param repo [String, Symbol, PlanMyStuff::Repo] repo name or key
|
|
61
|
+
# @param name [String] label name
|
|
62
|
+
#
|
|
63
|
+
# @return [Boolean]
|
|
64
|
+
#
|
|
65
|
+
def exists?(repo:, name:)
|
|
66
|
+
PlanMyStuff.client.rest(:label, repo, name)
|
|
67
|
+
true
|
|
68
|
+
rescue PlanMyStuff::APIError => e
|
|
69
|
+
raise unless e.status == 404
|
|
70
|
+
|
|
71
|
+
false
|
|
55
72
|
end
|
|
56
73
|
|
|
57
74
|
# Removes labels from a GitHub issue.
|
|
@@ -62,7 +79,7 @@ module PlanMyStuff
|
|
|
62
79
|
#
|
|
63
80
|
# @return [Array<Array<PlanMyStuff::Label>>] results of each removal
|
|
64
81
|
#
|
|
65
|
-
def remove(issue:, labels:, user: nil)
|
|
82
|
+
def remove!(issue:, labels:, user: nil)
|
|
66
83
|
label_names = Array.wrap(labels)
|
|
67
84
|
|
|
68
85
|
results = label_names.map do |label|
|
|
@@ -70,7 +87,7 @@ module PlanMyStuff
|
|
|
70
87
|
result.map { |gh_label| build(gh_label, issue: issue) }
|
|
71
88
|
end
|
|
72
89
|
|
|
73
|
-
|
|
90
|
+
PlanMyStuff::Cache.delete_issue(issue.repo, issue.number)
|
|
74
91
|
|
|
75
92
|
PlanMyStuff::Notifications.instrument(
|
|
76
93
|
'label.removed', issue, user: user, labels: label_names,
|
|
@@ -81,9 +98,8 @@ module PlanMyStuff
|
|
|
81
98
|
|
|
82
99
|
private
|
|
83
100
|
|
|
84
|
-
# Creates a label, tolerating the 422 "already exists" race that
|
|
85
|
-
#
|
|
86
|
-
# and this write.
|
|
101
|
+
# Creates a label, tolerating the 422 "already exists" race that occurs when a concurrent ensure! slipped in
|
|
102
|
+
# between our 404 read and this write.
|
|
87
103
|
#
|
|
88
104
|
# @param client [PlanMyStuff::Client]
|
|
89
105
|
# @param repo [String, Symbol]
|
|
@@ -93,7 +109,7 @@ module PlanMyStuff
|
|
|
93
109
|
#
|
|
94
110
|
# @return [void]
|
|
95
111
|
#
|
|
96
|
-
def create_label(client, repo, name, color, description)
|
|
112
|
+
def create_label!(client, repo, name, color, description)
|
|
97
113
|
options = {}
|
|
98
114
|
options[:description] = description if description
|
|
99
115
|
|
|
@@ -117,8 +133,8 @@ module PlanMyStuff
|
|
|
117
133
|
end
|
|
118
134
|
end
|
|
119
135
|
|
|
120
|
-
# Serializes the label to a JSON-safe hash, excluding the back-reference
|
|
121
|
-
#
|
|
136
|
+
# Serializes the label to a JSON-safe hash, excluding the back-reference to the parent issue to prevent
|
|
137
|
+
# recursive serialization cycles.
|
|
122
138
|
#
|
|
123
139
|
# @return [Hash]
|
|
124
140
|
#
|
data/lib/plan_my_stuff/link.rb
CHANGED
|
@@ -3,14 +3,12 @@
|
|
|
3
3
|
require 'active_model'
|
|
4
4
|
|
|
5
5
|
module PlanMyStuff
|
|
6
|
-
# Value object representing a typed relationship between two issues.
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
# +remove_*+ counterparts so callers can render activity-log lines
|
|
10
|
-
# like +"Added parent issue: owner/repo#42"+.
|
|
6
|
+
# Value object representing a typed relationship between two issues. Built by +Issue#add_related!+,
|
|
7
|
+
# +#add_blocker!+, +#add_sub_issue!+, +#set_parent!+, and +#mark_duplicate!+; also returned from their +remove_*+
|
|
8
|
+
# counterparts so callers can render activity-log lines like +"Added parent issue: owner/repo#42"+.
|
|
11
9
|
#
|
|
12
|
-
# Metadata-backed types live in +IssueMetadata#links+; native types
|
|
13
|
-
#
|
|
10
|
+
# Metadata-backed types live in +IssueMetadata#links+; native types are routed through GitHub APIs and never
|
|
11
|
+
# persisted in our metadata.
|
|
14
12
|
class Link
|
|
15
13
|
METADATA_TYPES = %w[related].freeze
|
|
16
14
|
NATIVE_TYPES = %w[blocking blocked_by parent sub_ticket duplicate_of].freeze
|
|
@@ -32,11 +30,10 @@ module PlanMyStuff
|
|
|
32
30
|
validates :repo, presence: true
|
|
33
31
|
|
|
34
32
|
class << self
|
|
35
|
-
# Builds and validates a +Link+ from an +Issue+-like object, another
|
|
36
|
-
#
|
|
37
|
-
#
|
|
38
|
-
#
|
|
39
|
-
# fields, +ArgumentError+ when +input+ is an unsupported shape.
|
|
33
|
+
# Builds and validates a +Link+ from an +Issue+-like object, another +Link+, or a hash. +type:+ fills in when
|
|
34
|
+
# the input does not carry one; +source_repo+ fills in when the hash input does not carry one. Raises
|
|
35
|
+
# +ActiveModel::ValidationError+ on missing/invalid fields, +ArgumentError+ when +input+ is an unsupported
|
|
36
|
+
# shape.
|
|
40
37
|
#
|
|
41
38
|
# @param input [PlanMyStuff::Issue, PlanMyStuff::Link, Hash]
|
|
42
39
|
# @param type [String, Symbol, nil]
|
|
@@ -44,14 +41,14 @@ module PlanMyStuff
|
|
|
44
41
|
#
|
|
45
42
|
# @return [PlanMyStuff::Link]
|
|
46
43
|
#
|
|
47
|
-
def build(input, type: nil, source_repo: nil)
|
|
44
|
+
def build!(input, type: nil, source_repo: nil)
|
|
48
45
|
return input if input.is_a?(PlanMyStuff::Link)
|
|
49
46
|
|
|
50
47
|
link =
|
|
51
48
|
if input.is_a?(Hash)
|
|
52
49
|
build_from_hash(input, type: type, source_repo: source_repo)
|
|
53
50
|
else
|
|
54
|
-
build_from_issue_like(input, type: type)
|
|
51
|
+
build_from_issue_like!(input, type: type)
|
|
55
52
|
end
|
|
56
53
|
|
|
57
54
|
link.validate!
|
|
@@ -60,8 +57,11 @@ module PlanMyStuff
|
|
|
60
57
|
|
|
61
58
|
private
|
|
62
59
|
|
|
60
|
+
# @raise [ArgumentError] if input does not respond to #number and #repo
|
|
61
|
+
#
|
|
63
62
|
# @return [PlanMyStuff::Link]
|
|
64
|
-
|
|
63
|
+
#
|
|
64
|
+
def build_from_issue_like!(input, type:)
|
|
65
65
|
if !input.respond_to?(:number) || !input.respond_to?(:repo)
|
|
66
66
|
raise(ArgumentError, "Cannot build Link from #{input.class}")
|
|
67
67
|
end
|
|
@@ -5,8 +5,10 @@ require 'active_support/core_ext/hash/deep_merge'
|
|
|
5
5
|
module PlanMyStuff
|
|
6
6
|
module Markdown
|
|
7
7
|
class << self
|
|
8
|
-
# Renders markdown text to HTML using the configured renderer.
|
|
9
|
-
#
|
|
8
|
+
# Renders markdown text to HTML using the configured renderer. Per-call options are deep-merged on top of
|
|
9
|
+
# config.markdown_options.
|
|
10
|
+
#
|
|
11
|
+
# @raise [ArgumentError] if the configured renderer is not an option
|
|
10
12
|
#
|
|
11
13
|
# @param text [String] raw markdown text
|
|
12
14
|
# @param options [Hash] renderer-specific options (merged over config defaults)
|
|
@@ -22,9 +24,9 @@ module PlanMyStuff
|
|
|
22
24
|
|
|
23
25
|
case config.markdown_renderer
|
|
24
26
|
when :commonmarker
|
|
25
|
-
render_commonmarker(text, merged)
|
|
27
|
+
render_commonmarker!(text, merged)
|
|
26
28
|
when :redcarpet
|
|
27
|
-
render_redcarpet(text, merged)
|
|
29
|
+
render_redcarpet!(text, merged)
|
|
28
30
|
when nil
|
|
29
31
|
"<code>#{ERB::Util.html_escape(text)}</code>"
|
|
30
32
|
else
|
|
@@ -37,12 +39,14 @@ module PlanMyStuff
|
|
|
37
39
|
|
|
38
40
|
private
|
|
39
41
|
|
|
42
|
+
# @raise [PlanMyStuff::Error] if commonmarker gem is not available
|
|
43
|
+
#
|
|
40
44
|
# @param text [String]
|
|
41
45
|
# @param options [Hash]
|
|
42
46
|
#
|
|
43
47
|
# @return [String]
|
|
44
48
|
#
|
|
45
|
-
def render_commonmarker(text, options)
|
|
49
|
+
def render_commonmarker!(text, options)
|
|
46
50
|
require('commonmarker') unless defined?(Commonmarker)
|
|
47
51
|
|
|
48
52
|
if options.empty?
|
|
@@ -58,12 +62,14 @@ module PlanMyStuff
|
|
|
58
62
|
)
|
|
59
63
|
end
|
|
60
64
|
|
|
65
|
+
# @raise [PlanMyStuff::Error] if redcarpet gem is not available
|
|
66
|
+
#
|
|
61
67
|
# @param text [String]
|
|
62
68
|
# @param options [Hash]
|
|
63
69
|
#
|
|
64
70
|
# @return [String]
|
|
65
71
|
#
|
|
66
|
-
def render_redcarpet(text, options)
|
|
72
|
+
def render_redcarpet!(text, options)
|
|
67
73
|
require('redcarpet') unless defined?(Redcarpet)
|
|
68
74
|
|
|
69
75
|
options = options.dup
|
|
@@ -30,12 +30,14 @@ module PlanMyStuff
|
|
|
30
30
|
|
|
31
31
|
# Serializes a metadata hash and body into the stored format
|
|
32
32
|
#
|
|
33
|
+
# @raise [ArgumentError] if metadata is not a Hash or PlanMyStuff::CustomFields
|
|
34
|
+
#
|
|
33
35
|
# @param metadata [Hash, PlanMyStuff::CustomFields]
|
|
34
36
|
# @param body [String]
|
|
35
37
|
#
|
|
36
38
|
# @return [String]
|
|
37
39
|
#
|
|
38
|
-
def serialize(metadata, body)
|
|
40
|
+
def serialize!(metadata, body)
|
|
39
41
|
if !metadata.is_a?(Hash) && !metadata.is_a?(PlanMyStuff::CustomFields)
|
|
40
42
|
raise(ArgumentError, "metadata must be a Hash or PlanMyStuff::CustomFields, got #{metadata.class}")
|
|
41
43
|
end
|
|
@@ -113,7 +113,7 @@ module PlanMyStuff
|
|
|
113
113
|
logger = rails_logger
|
|
114
114
|
return if logger.nil?
|
|
115
115
|
|
|
116
|
-
logger.debug { "[
|
|
116
|
+
logger.debug { "[PlanMyStuff] #{EVENT_PREFIX}.#{event} #{log_fields(payload)}" }
|
|
117
117
|
end
|
|
118
118
|
|
|
119
119
|
# @return [Logger, nil]
|
|
@@ -20,11 +20,11 @@ module PlanMyStuff
|
|
|
20
20
|
#
|
|
21
21
|
# @return [Array<PlanMyStuff::ProjectItem>] removed items
|
|
22
22
|
#
|
|
23
|
-
def perform(project_number: nil, now: Time.current)
|
|
23
|
+
def perform!(project_number: nil, now: Time.current)
|
|
24
24
|
config = PlanMyStuff.configuration
|
|
25
25
|
return [] unless config.pipeline_completion_purge_enabled
|
|
26
26
|
|
|
27
|
-
number = PlanMyStuff::Pipeline.resolve_pipeline_project_number(project_number)
|
|
27
|
+
number = PlanMyStuff::Pipeline.resolve_pipeline_project_number!(project_number)
|
|
28
28
|
project = PlanMyStuff::Project.find(number)
|
|
29
29
|
|
|
30
30
|
completed_status = PlanMyStuff::Pipeline.resolve_status_name(
|
|
@@ -52,7 +52,7 @@ module PlanMyStuff
|
|
|
52
52
|
# @return [PlanMyStuff::ProjectItem, nil] matching item, or nil if not found
|
|
53
53
|
#
|
|
54
54
|
def find_project_item(issue_number, _repo: nil, project_number: nil)
|
|
55
|
-
number = PlanMyStuff::Pipeline.resolve_pipeline_project_number(project_number)
|
|
55
|
+
number = PlanMyStuff::Pipeline.resolve_pipeline_project_number!(project_number)
|
|
56
56
|
project = PlanMyStuff::Project.find(number)
|
|
57
57
|
|
|
58
58
|
project.items.find { |item| item.number == issue_number }
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'active_support/notifications'
|
|
4
3
|
require_relative 'pipeline/completed_sweep'
|
|
5
4
|
require_relative 'pipeline/issue_linker'
|
|
6
5
|
require_relative 'pipeline/status'
|
|
@@ -9,27 +8,23 @@ require_relative 'pipeline/testing'
|
|
|
9
8
|
module PlanMyStuff
|
|
10
9
|
# High-level orchestration layer for the release pipeline.
|
|
11
10
|
#
|
|
12
|
-
# Named lifecycle methods wrap the low-level +ProjectItem.move_to!+ calls,
|
|
13
|
-
#
|
|
14
|
-
# +ActiveSupport::Notifications+ events on every transition.
|
|
11
|
+
# Named lifecycle methods wrap the low-level +ProjectItem.move_to!+ calls, resolve configurable status aliases, and
|
|
12
|
+
# fire +ActiveSupport::Notifications+ events on every transition.
|
|
15
13
|
#
|
|
16
14
|
# No transition enforcement -- any status can move to any other.
|
|
17
15
|
#
|
|
18
16
|
module Pipeline
|
|
19
17
|
module_function
|
|
20
18
|
|
|
21
|
-
# Raises +PlanMyStuff::PendingApprovalsError+ when +issue+ has
|
|
22
|
-
#
|
|
23
|
-
# either have no approvers required or are fully approved.
|
|
19
|
+
# Raises +PlanMyStuff::PendingApprovalsError+ when +issue+ has pending manager approvals. No-op for +nil+ issues or
|
|
20
|
+
# issues that either have no approvers required or are fully approved.
|
|
24
21
|
#
|
|
25
|
-
# Called at the top of every forward transition (+take!+,
|
|
26
|
-
# +
|
|
27
|
-
# +
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
# batch/automated paths would abort entire deploys on a single
|
|
32
|
-
# approval revoke.
|
|
22
|
+
# Called at the top of every forward transition (+take!+, +mark_in_review!+, +request_testing!+,
|
|
23
|
+
# +mark_ready_for_release!+). Batch / CI-driven transitions (+start_deployment!+, +complete_deployment!+) and
|
|
24
|
+
# reverse transitions (+remove!+) are intentionally NOT gated -- earlier forward transitions already required
|
|
25
|
+
# approval, and gating batch/automated paths would abort entire deploys on a single approval revoke.
|
|
26
|
+
#
|
|
27
|
+
# @raise [PlanMyStuff::PendingApprovalsError] when +issue+ has pending approvals
|
|
33
28
|
#
|
|
34
29
|
# @param issue [PlanMyStuff::Issue, nil]
|
|
35
30
|
#
|
|
@@ -48,8 +43,8 @@ module PlanMyStuff
|
|
|
48
43
|
))
|
|
49
44
|
end
|
|
50
45
|
|
|
51
|
-
# Resolves a canonical status name to its configured display alias,
|
|
52
|
-
#
|
|
46
|
+
# Resolves a canonical status name to its configured display alias, falling back to the canonical name when no
|
|
47
|
+
# alias is configured.
|
|
53
48
|
#
|
|
54
49
|
# @param canonical [String] one of the +Pipeline::Status+ constants
|
|
55
50
|
#
|
|
@@ -59,36 +54,44 @@ module PlanMyStuff
|
|
|
59
54
|
PlanMyStuff.configuration.pipeline_statuses.fetch(canonical, canonical)
|
|
60
55
|
end
|
|
61
56
|
|
|
62
|
-
# Fires
|
|
57
|
+
# Fires a +plan_my_stuff.pipeline.<event>+ notification.
|
|
63
58
|
#
|
|
64
|
-
#
|
|
65
|
-
#
|
|
59
|
+
# When +event+ is a canonical +Pipeline::Status+ name, the event key is derived via +Status.key_for+ and the
|
|
60
|
+
# canonical status is added to the payload as +:status+. Otherwise +event+ is used verbatim as the suffix (e.g.
|
|
61
|
+
# +"removed"+, +"removed_late"+, +"testing"+).
|
|
62
|
+
#
|
|
63
|
+
# @param event [String] status name or literal event suffix
|
|
64
|
+
# @param project_item [PlanMyStuff::BaseProjectItem]
|
|
66
65
|
# @param extra [Hash] additional payload entries
|
|
67
66
|
#
|
|
68
67
|
# @return [void]
|
|
69
68
|
#
|
|
70
|
-
def instrument(
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}.merge(extra)
|
|
69
|
+
def instrument(event, project_item, **extra)
|
|
70
|
+
extra_to_use = { **extra }
|
|
71
|
+
event_to_use = event
|
|
72
|
+
if PlanMyStuff::Pipeline::Status::ALL.include?(event_to_use)
|
|
73
|
+
extra_to_use = { status: event, **extra_to_use }
|
|
74
|
+
event_to_use = PlanMyStuff::Pipeline::Status.key_for(event_to_use)
|
|
75
|
+
end
|
|
78
76
|
|
|
79
|
-
|
|
77
|
+
PlanMyStuff::Notifications.instrument(
|
|
78
|
+
"pipeline.#{event_to_use}",
|
|
79
|
+
project_item,
|
|
80
|
+
issue_number: project_item.number,
|
|
81
|
+
**extra_to_use,
|
|
82
|
+
)
|
|
80
83
|
end
|
|
81
84
|
|
|
82
|
-
# Returns the pipeline project number from the explicit argument,
|
|
83
|
-
# +
|
|
85
|
+
# Returns the pipeline project number from the explicit argument, +pipeline_project_number+, or
|
|
86
|
+
# +default_project_number+.
|
|
87
|
+
#
|
|
88
|
+
# @raise [ArgumentError] if no project number can be resolved
|
|
84
89
|
#
|
|
85
90
|
# @param project_number [Integer, nil]
|
|
86
91
|
#
|
|
87
92
|
# @return [Integer]
|
|
88
93
|
#
|
|
89
|
-
|
|
90
|
-
#
|
|
91
|
-
def resolve_pipeline_project_number(project_number = nil)
|
|
94
|
+
def resolve_pipeline_project_number!(project_number = nil)
|
|
92
95
|
config = PlanMyStuff.configuration
|
|
93
96
|
result = project_number || config.pipeline_project_number || config.default_project_number
|
|
94
97
|
|
|
@@ -97,14 +100,12 @@ module PlanMyStuff
|
|
|
97
100
|
result
|
|
98
101
|
end
|
|
99
102
|
|
|
100
|
-
# Removes a project item from the pipeline project entirely.
|
|
101
|
-
#
|
|
102
|
-
# decide whether the removal happened late in the lifecycle.
|
|
103
|
+
# Removes a project item from the pipeline project entirely. Captures the prior status before deletion so
|
|
104
|
+
# subscribers can decide whether the removal happened late in the lifecycle.
|
|
103
105
|
#
|
|
104
|
-
# Always fires +plan_my_stuff.pipeline.removed+. Additionally fires
|
|
105
|
-
# +
|
|
106
|
-
#
|
|
107
|
-
# configured status aliases). +nil+ status is treated as not-late.
|
|
106
|
+
# Always fires +plan_my_stuff.pipeline.removed+. Additionally fires +plan_my_stuff.pipeline.removed_late+ when
|
|
107
|
+
# +prior_status+ is past "Started" (i.e. anything other than "Started", accounting for configured status aliases).
|
|
108
|
+
# +nil+ status is treated as not-late.
|
|
108
109
|
#
|
|
109
110
|
# @param project_item [PlanMyStuff::ProjectItem]
|
|
110
111
|
#
|
|
@@ -114,24 +115,14 @@ module PlanMyStuff
|
|
|
114
115
|
prior_status = project_item.status
|
|
115
116
|
result = project_item.destroy!
|
|
116
117
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
issue_number: project_item.number,
|
|
120
|
-
prior_status: prior_status,
|
|
121
|
-
timestamp: Time.current,
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
ActiveSupport::Notifications.instrument('plan_my_stuff.pipeline.removed', payload)
|
|
125
|
-
|
|
126
|
-
if late_removal?(prior_status)
|
|
127
|
-
ActiveSupport::Notifications.instrument('plan_my_stuff.pipeline.removed_late', payload)
|
|
128
|
-
end
|
|
118
|
+
instrument('removed', project_item, prior_status: prior_status)
|
|
119
|
+
instrument('removed_late', project_item, prior_status: prior_status) if late_removal?(prior_status)
|
|
129
120
|
|
|
130
121
|
result
|
|
131
122
|
end
|
|
132
123
|
|
|
133
|
-
# Returns true when +prior_status+ is past "Started" in the pipeline.
|
|
134
|
-
#
|
|
124
|
+
# Returns true when +prior_status+ is past "Started" in the pipeline. Honors configured status aliases. +nil+ is
|
|
125
|
+
# treated as not-late.
|
|
135
126
|
#
|
|
136
127
|
# @param prior_status [String, nil]
|
|
137
128
|
#
|
|
@@ -144,13 +135,11 @@ module PlanMyStuff
|
|
|
144
135
|
prior_status != started
|
|
145
136
|
end
|
|
146
137
|
|
|
147
|
-
# Returns true when +project_item+ is at a release-cycle status
|
|
148
|
-
#
|
|
149
|
-
# Honors configured status aliases.
|
|
138
|
+
# Returns true when +project_item+ is at a release-cycle status ("Ready for Release", "Release in Progress", or
|
|
139
|
+
# "Completed"). Honors configured status aliases.
|
|
150
140
|
#
|
|
151
|
-
# Used to lock items against webhook-driven removals once they
|
|
152
|
-
#
|
|
153
|
-
# item out of "Release in Progress".
|
|
141
|
+
# Used to lock items against webhook-driven removals once they enter the release path -- a stray unassign should
|
|
142
|
+
# not yank an item out of "Release in Progress".
|
|
154
143
|
#
|
|
155
144
|
# @param project_item [PlanMyStuff::BaseProjectItem]
|
|
156
145
|
#
|
|
@@ -196,9 +185,8 @@ module PlanMyStuff
|
|
|
196
185
|
result
|
|
197
186
|
end
|
|
198
187
|
|
|
199
|
-
# Marks a project item as in testing by setting the +Testing+ custom
|
|
200
|
-
#
|
|
201
|
-
# +Status+ -- testing runs orthogonally to +In Review+.
|
|
188
|
+
# Marks a project item as in testing by setting the +Testing+ custom single-select field to its active value. Does
|
|
189
|
+
# NOT change the item's +Status+ -- testing runs orthogonally to +In Review+.
|
|
202
190
|
#
|
|
203
191
|
# @param project_item [PlanMyStuff::ProjectItem]
|
|
204
192
|
#
|
|
@@ -210,14 +198,7 @@ module PlanMyStuff
|
|
|
210
198
|
value = PlanMyStuff.configuration.pipeline_testing_values.fetch(:active)
|
|
211
199
|
result = project_item.update_single_select_field!(field_name, value)
|
|
212
200
|
|
|
213
|
-
|
|
214
|
-
'plan_my_stuff.pipeline.testing',
|
|
215
|
-
project_item: project_item,
|
|
216
|
-
issue_number: project_item.number,
|
|
217
|
-
field_name: field_name,
|
|
218
|
-
value: value,
|
|
219
|
-
timestamp: Time.current,
|
|
220
|
-
)
|
|
201
|
+
instrument('testing', project_item, field_name: field_name, value: value)
|
|
221
202
|
|
|
222
203
|
result
|
|
223
204
|
end
|
|
@@ -237,14 +218,12 @@ module PlanMyStuff
|
|
|
237
218
|
result
|
|
238
219
|
end
|
|
239
220
|
|
|
240
|
-
# Finds ALL items at "Ready for Release" in the pipeline project and
|
|
241
|
-
#
|
|
221
|
+
# Finds ALL items at "Ready for Release" in the pipeline project and moves each to "Release in Progress". Fires an
|
|
222
|
+
# event per item.
|
|
242
223
|
#
|
|
243
|
-
# When +commit_sha+ is given (the merge_commit_sha of the PR into
|
|
244
|
-
#
|
|
245
|
-
#
|
|
246
|
-
# this sha against the configured +production_commit_sha+ to decide
|
|
247
|
-
# which items to auto-complete, so this is the only commit sha in the
|
|
224
|
+
# When +commit_sha+ is given (the merge_commit_sha of the PR into +production+), it is stamped onto every moved
|
|
225
|
+
# item's linked issue metadata. +AwsController#handle_deployment_completed+ later matches this sha against the
|
|
226
|
+
# configured +production_commit_sha+ to decide which items to auto-complete, so this is the only commit sha in the
|
|
248
227
|
# release cycle that matters.
|
|
249
228
|
#
|
|
250
229
|
# @param project_number [Integer, nil]
|
|
@@ -253,7 +232,7 @@ module PlanMyStuff
|
|
|
253
232
|
# @return [Array<PlanMyStuff::ProjectItem>]
|
|
254
233
|
#
|
|
255
234
|
def start_deployment!(project_number: nil, commit_sha: nil)
|
|
256
|
-
number = resolve_pipeline_project_number(project_number)
|
|
235
|
+
number = resolve_pipeline_project_number!(project_number)
|
|
257
236
|
project = PlanMyStuff::Project.find(number)
|
|
258
237
|
|
|
259
238
|
ready_status = resolve_status_name(PlanMyStuff::Pipeline::Status::READY_FOR_RELEASE)
|
|
@@ -275,9 +254,8 @@ module PlanMyStuff
|
|
|
275
254
|
items
|
|
276
255
|
end
|
|
277
256
|
|
|
278
|
-
# Moves a project item to "Completed" if the linked issue has
|
|
279
|
-
#
|
|
280
|
-
# (item stays at "Release in Progress").
|
|
257
|
+
# Moves a project item to "Completed" if the linked issue has +auto_complete+ enabled. Returns +nil+ when
|
|
258
|
+
# auto-complete is off (item stays at "Release in Progress").
|
|
281
259
|
#
|
|
282
260
|
# @param project_item [PlanMyStuff::ProjectItem]
|
|
283
261
|
# @param deployment_id [String, nil]
|
|
@@ -44,7 +44,7 @@ module PlanMyStuff
|
|
|
44
44
|
project_id = new_project[:id]
|
|
45
45
|
project_number = new_project[:number]
|
|
46
46
|
|
|
47
|
-
serialized_readme = PlanMyStuff::MetadataParser.serialize(project_metadata.to_h, readme)
|
|
47
|
+
serialized_readme = PlanMyStuff::MetadataParser.serialize!(project_metadata.to_h, readme)
|
|
48
48
|
update_input = { projectId: project_id, readme: serialized_readme }
|
|
49
49
|
update_input[:shortDescription] = description if description.present?
|
|
50
50
|
|
|
@@ -60,12 +60,12 @@ module PlanMyStuff
|
|
|
60
60
|
# project - call +TestingProject.find+ for those, or +BaseProject.find+
|
|
61
61
|
# if you want either type.
|
|
62
62
|
#
|
|
63
|
+
# @raise [ArgumentError] if the project is a testing project
|
|
64
|
+
#
|
|
63
65
|
# @param number [Integer]
|
|
64
66
|
# @param paginate [Symbol] :auto (default) or :cursor
|
|
65
67
|
# @param cursor [String, nil] pagination cursor for :cursor mode
|
|
66
68
|
#
|
|
67
|
-
# @raise [ArgumentError] if the project is a testing project
|
|
68
|
-
#
|
|
69
69
|
# @return [PlanMyStuff::Project]
|
|
70
70
|
#
|
|
71
71
|
def find(number, paginate: :auto, cursor: nil)
|
|
@@ -91,7 +91,7 @@ module PlanMyStuff
|
|
|
91
91
|
|
|
92
92
|
private
|
|
93
93
|
|
|
94
|
-
# Returns the item class used to build items for
|
|
94
|
+
# Returns the item class used to build items for regular projects.
|
|
95
95
|
#
|
|
96
96
|
# @return [Class]
|
|
97
97
|
#
|
|
@@ -10,7 +10,7 @@ module PlanMyStuff
|
|
|
10
10
|
# (T-049), which will assign non-GitHub users (e.g. internal QA
|
|
11
11
|
# accounts) to draft items via the +pms_assignee+ field.
|
|
12
12
|
#
|
|
13
|
-
class ProjectItemMetadata < BaseMetadata
|
|
13
|
+
class ProjectItemMetadata < PlanMyStuff::BaseMetadata
|
|
14
14
|
# @return [Integer, nil] consuming app user id of the PMS-side assignee
|
|
15
15
|
attr_accessor :pms_assignee
|
|
16
16
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module PlanMyStuff
|
|
4
4
|
# Metadata for regular (non-testing) projects.
|
|
5
|
-
class ProjectMetadata < BaseProjectMetadata
|
|
5
|
+
class ProjectMetadata < PlanMyStuff::BaseProjectMetadata
|
|
6
6
|
class << self
|
|
7
7
|
# Builds a ProjectMetadata from a parsed hash (e.g. from MetadataParser)
|
|
8
8
|
#
|
|
@@ -63,7 +63,7 @@ module PlanMyStuff
|
|
|
63
63
|
return if @issue.labels.include?(label)
|
|
64
64
|
|
|
65
65
|
PlanMyStuff::Label.ensure!(repo: @issue.repo, name: label)
|
|
66
|
-
PlanMyStuff::Label.add(issue: @issue, labels: [label])
|
|
66
|
+
PlanMyStuff::Label.add!(issue: @issue, labels: [label])
|
|
67
67
|
end
|
|
68
68
|
end
|
|
69
69
|
end
|