plan_my_stuff 0.1.0 → 1.0.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.
Files changed (113) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +595 -0
  3. data/CONFIGURATION.md +487 -0
  4. data/README.md +612 -88
  5. data/app/controllers/plan_my_stuff/application_controller.rb +27 -5
  6. data/app/controllers/plan_my_stuff/comments_controller.rb +50 -19
  7. data/app/controllers/plan_my_stuff/issues/approvals_controller.rb +127 -0
  8. data/app/controllers/plan_my_stuff/issues/closures_controller.rb +53 -0
  9. data/app/controllers/plan_my_stuff/issues/links_controller.rb +129 -0
  10. data/app/controllers/plan_my_stuff/issues/takes_controller.rb +161 -0
  11. data/app/controllers/plan_my_stuff/issues/testings_controller.rb +82 -0
  12. data/app/controllers/plan_my_stuff/issues/viewers_controller.rb +62 -0
  13. data/app/controllers/plan_my_stuff/issues/waitings_controller.rb +55 -0
  14. data/app/controllers/plan_my_stuff/issues_controller.rb +53 -70
  15. data/app/controllers/plan_my_stuff/labels_controller.rb +32 -10
  16. data/app/controllers/plan_my_stuff/project_items/assignments_controller.rb +88 -0
  17. data/app/controllers/plan_my_stuff/project_items/statuses_controller.rb +44 -0
  18. data/app/controllers/plan_my_stuff/project_items_controller.rb +32 -69
  19. data/app/controllers/plan_my_stuff/projects_controller.rb +81 -3
  20. data/app/controllers/plan_my_stuff/testing_project_items/results_controller.rb +67 -0
  21. data/app/controllers/plan_my_stuff/testing_project_items_controller.rb +49 -0
  22. data/app/controllers/plan_my_stuff/testing_projects_controller.rb +121 -0
  23. data/app/controllers/plan_my_stuff/webhooks/aws_controller.rb +202 -0
  24. data/app/controllers/plan_my_stuff/webhooks/github_controller.rb +371 -0
  25. data/app/jobs/plan_my_stuff/application_job.rb +8 -0
  26. data/app/jobs/plan_my_stuff/reminders_sweep_job.rb +75 -0
  27. data/app/views/plan_my_stuff/comments/edit.html.erb +1 -3
  28. data/app/views/plan_my_stuff/comments/partials/_form.html.erb +8 -0
  29. data/app/views/plan_my_stuff/issues/edit.html.erb +2 -4
  30. data/app/views/plan_my_stuff/issues/index.html.erb +5 -5
  31. data/app/views/plan_my_stuff/issues/new.html.erb +2 -4
  32. data/app/views/plan_my_stuff/issues/partials/_approvals.html.erb +108 -0
  33. data/app/views/plan_my_stuff/issues/partials/_form.html.erb +11 -6
  34. data/app/views/plan_my_stuff/issues/partials/_labels.html.erb +4 -3
  35. data/app/views/plan_my_stuff/issues/partials/_links.html.erb +113 -0
  36. data/app/views/plan_my_stuff/issues/partials/_viewers.html.erb +4 -3
  37. data/app/views/plan_my_stuff/issues/show.html.erb +67 -6
  38. data/app/views/plan_my_stuff/partials/_flash.html.erb +3 -0
  39. data/app/views/plan_my_stuff/projects/edit.html.erb +5 -0
  40. data/app/views/plan_my_stuff/projects/index.html.erb +18 -2
  41. data/app/views/plan_my_stuff/projects/new.html.erb +5 -0
  42. data/app/views/plan_my_stuff/projects/partials/_form.html.erb +30 -0
  43. data/app/views/plan_my_stuff/projects/show.html.erb +30 -11
  44. data/app/views/plan_my_stuff/testing_project_items/new.html.erb +10 -0
  45. data/app/views/plan_my_stuff/testing_project_items/results/new.html.erb +20 -0
  46. data/app/views/plan_my_stuff/testing_projects/edit.html.erb +5 -0
  47. data/app/views/plan_my_stuff/testing_projects/new.html.erb +5 -0
  48. data/app/views/plan_my_stuff/testing_projects/partials/_form.html.erb +40 -0
  49. data/app/views/plan_my_stuff/testing_projects/partials/_item.html.erb +52 -0
  50. data/app/views/plan_my_stuff/testing_projects/partials/items/_form.html.erb +36 -0
  51. data/app/views/plan_my_stuff/testing_projects/show.html.erb +65 -0
  52. data/config/routes.rb +43 -15
  53. data/lib/generators/plan_my_stuff/install/templates/initializer.rb +302 -20
  54. data/lib/plan_my_stuff/application_record.rb +158 -1
  55. data/lib/plan_my_stuff/approval.rb +88 -0
  56. data/lib/plan_my_stuff/archive/sweep.rb +85 -0
  57. data/lib/plan_my_stuff/archive.rb +12 -0
  58. data/lib/plan_my_stuff/attachment.rb +83 -0
  59. data/lib/plan_my_stuff/attachment_uploader.rb +245 -0
  60. data/lib/plan_my_stuff/aws_sns_simulator.rb +116 -0
  61. data/lib/plan_my_stuff/base_metadata.rb +25 -28
  62. data/lib/plan_my_stuff/base_project.rb +502 -0
  63. data/lib/plan_my_stuff/base_project_extractions/graphql_hydration.rb +186 -0
  64. data/lib/plan_my_stuff/base_project_item.rb +588 -0
  65. data/lib/plan_my_stuff/base_project_metadata.rb +16 -0
  66. data/lib/plan_my_stuff/cache.rb +197 -0
  67. data/lib/plan_my_stuff/client.rb +139 -64
  68. data/lib/plan_my_stuff/comment.rb +225 -100
  69. data/lib/plan_my_stuff/comment_metadata.rb +68 -5
  70. data/lib/plan_my_stuff/configuration.rb +459 -28
  71. data/lib/plan_my_stuff/custom_fields.rb +96 -12
  72. data/lib/plan_my_stuff/engine.rb +14 -2
  73. data/lib/plan_my_stuff/errors.rb +65 -5
  74. data/lib/plan_my_stuff/graphql/queries.rb +454 -0
  75. data/lib/plan_my_stuff/issue.rb +1097 -166
  76. data/lib/plan_my_stuff/issue_extractions/approvals.rb +370 -0
  77. data/lib/plan_my_stuff/issue_extractions/links.rb +525 -0
  78. data/lib/plan_my_stuff/issue_extractions/viewers.rb +75 -0
  79. data/lib/plan_my_stuff/issue_extractions/waiting.rb +171 -0
  80. data/lib/plan_my_stuff/issue_field.rb +126 -0
  81. data/lib/plan_my_stuff/issue_field_translation.rb +67 -0
  82. data/lib/plan_my_stuff/issue_field_value_set.rb +68 -0
  83. data/lib/plan_my_stuff/issue_metadata.rb +132 -21
  84. data/lib/plan_my_stuff/label.rb +100 -13
  85. data/lib/plan_my_stuff/link.rb +144 -0
  86. data/lib/plan_my_stuff/markdown.rb +13 -7
  87. data/lib/plan_my_stuff/metadata_parser.rb +51 -12
  88. data/lib/plan_my_stuff/notifications.rb +148 -0
  89. data/lib/plan_my_stuff/pipeline/completed_sweep.rb +46 -0
  90. data/lib/plan_my_stuff/pipeline/issue_linker.rb +62 -0
  91. data/lib/plan_my_stuff/pipeline/status.rb +40 -0
  92. data/lib/plan_my_stuff/pipeline/testing.rb +23 -0
  93. data/lib/plan_my_stuff/pipeline.rb +310 -0
  94. data/lib/plan_my_stuff/project.rb +63 -465
  95. data/lib/plan_my_stuff/project_item.rb +3 -409
  96. data/lib/plan_my_stuff/project_item_metadata.rb +55 -0
  97. data/lib/plan_my_stuff/project_metadata.rb +47 -0
  98. data/lib/plan_my_stuff/reminders/closer.rb +70 -0
  99. data/lib/plan_my_stuff/reminders/fire.rb +129 -0
  100. data/lib/plan_my_stuff/reminders/sweep.rb +54 -0
  101. data/lib/plan_my_stuff/reminders.rb +12 -0
  102. data/lib/plan_my_stuff/repo.rb +145 -0
  103. data/lib/plan_my_stuff/test_helpers.rb +265 -25
  104. data/lib/plan_my_stuff/testing_project.rb +292 -0
  105. data/lib/plan_my_stuff/testing_project_item.rb +218 -0
  106. data/lib/plan_my_stuff/testing_project_metadata.rb +94 -0
  107. data/lib/plan_my_stuff/user_resolver.rb +24 -3
  108. data/lib/plan_my_stuff/verifier.rb +10 -0
  109. data/lib/plan_my_stuff/version.rb +2 -2
  110. data/lib/plan_my_stuff/webhook_replayer.rb +292 -0
  111. data/lib/plan_my_stuff.rb +55 -20
  112. data/lib/tasks/plan_my_stuff.rake +331 -0
  113. metadata +99 -4
@@ -1,10 +1,30 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'active_model'
4
+
3
5
  module PlanMyStuff
4
6
  # Dynamic accessor object for app-defined custom fields stored in metadata.
5
7
  # Backed by the config.custom_fields schema, provides both hash-style and
6
8
  # method-style access to field values.
9
+ #
10
+ # Includes ActiveModel::Validations for type checking, required field
11
+ # enforcement, and unknown field detection.
7
12
  class CustomFields
13
+ TYPE_MAP = {
14
+ string: [String],
15
+ integer: [Integer],
16
+ boolean: [TrueClass, FalseClass],
17
+ array: [Array],
18
+ hash: [Hash],
19
+ }.freeze
20
+
21
+ include ActiveModel::Validations
22
+
23
+ validate :validate_custom_fields
24
+
25
+ # @return [Hash{Symbol => Hash}]
26
+ attr_reader :schema
27
+
8
28
  # @param schema [Hash{Symbol => Hash}] field definitions from config.custom_fields
9
29
  # @param data [Hash] parsed field data from metadata JSON
10
30
  #
@@ -40,26 +60,90 @@ module PlanMyStuff
40
60
  to_h.to_json(...)
41
61
  end
42
62
 
63
+ # @param method_name [Symbol]
64
+ # @param include_private [Boolean]
65
+ #
66
+ # @return [Boolean]
67
+ #
68
+ def respond_to_missing?(method_name, include_private = false)
69
+ key = method_name.to_s.delete_suffix('=').to_sym
70
+ @schema.key?(key) || @data.key?(key) || super
71
+ end
72
+
73
+ # Dynamic reader/writer access to custom field values. Only resolves field
74
+ # names that appear in the schema or already have a value in @data; unknown
75
+ # names fall through to super (raising NoMethodError).
76
+ #
77
+ # @param method_name [Symbol]
78
+ # @param args [Array]
79
+ #
80
+ # @return [Object]
81
+ #
82
+ def method_missing(method_name, *args)
83
+ name = method_name.to_s
84
+
85
+ if name.end_with?('=')
86
+ key = name.delete_suffix('=').to_sym
87
+ if @schema.key?(key) || @data.key?(key)
88
+ return @data[key] = args.first
89
+ end
90
+ elsif @schema.key?(method_name) || @data.key?(method_name)
91
+ return @data[method_name]
92
+ end
93
+
94
+ super
95
+ end
96
+
43
97
  private
44
98
 
45
- def respond_to_missing?(method_name, include_private = false)
46
- key = method_name.to_s.delete_suffix('=').to_sym
47
- @schema.key?(key) || @data.key?(key) || super
99
+ # @return [void]
100
+ def validate_custom_fields
101
+ validate_unknown_fields
102
+ validate_required_fields
103
+ validate_field_types
48
104
  end
49
105
 
50
- def method_missing(method_name, *args)
51
- name = method_name.to_s
106
+ # @return [void]
107
+ def validate_unknown_fields
108
+ known_keys = @schema.keys
109
+ @data.each_key do |key|
110
+ if known_keys.exclude?(key)
111
+ errors.add(:base, "#{key} is not a recognized custom field")
112
+ end
113
+ end
114
+ end
52
115
 
53
- if name.end_with?('=')
54
- key = name.delete_suffix('=').to_sym
55
- if @schema.key?(key) || @data.key?(key)
56
- return @data[key] = args.first
116
+ # @return [void]
117
+ def validate_required_fields
118
+ @schema.each do |field_name, field_config|
119
+ if field_config[:required] && !@data.key?(field_name)
120
+ errors.add(:base, "#{field_name} is required")
57
121
  end
58
- elsif @schema.key?(method_name) || @data.key?(method_name)
59
- return @data[method_name]
60
122
  end
123
+ end
124
+
125
+ # @return [void]
126
+ def validate_field_types
127
+ @schema.each do |field_name, field_config|
128
+ next unless @data.key?(field_name)
129
+
130
+ value = @data[field_name]
131
+ next if value.nil?
61
132
 
62
- super
133
+ expected_type = field_config[:type]
134
+ ruby_types = TYPE_MAP[expected_type]
135
+ next if ruby_types.nil?
136
+ next if ruby_types.any? { |t| value.is_a?(t) }
137
+
138
+ expected_name =
139
+ if expected_type == :boolean
140
+ 'TrueClass/FalseClass'
141
+ else
142
+ ruby_types.first.name
143
+ end
144
+
145
+ errors.add(:base, "#{field_name} must be a #{expected_name}, got #{value.class}")
146
+ end
63
147
  end
64
148
  end
65
149
  end
@@ -4,8 +4,20 @@ module PlanMyStuff
4
4
  class Engine < ::Rails::Engine
5
5
  isolate_namespace PlanMyStuff
6
6
 
7
- rake_tasks do
8
- load(File.expand_path('../tasks/plan_my_stuff.rake', __dir__))
7
+ # Opt-in (via +config.eager_load_controllers_on_boot+): eager-load the engine's controllers regardless of the host
8
+ # app's eager_load setting. Without this, `defined?(PlanMyStuff::SomeController)` returns nil in host-app dev mode
9
+ # until something explicitly references the constant, since `defined?` does not trigger Zeitwerk autoload.
10
+ config.after_initialize do
11
+ next unless PlanMyStuff.configuration.eager_load_controllers_on_boot
12
+
13
+ controllers_dir = File.expand_path('../../app/controllers', __dir__)
14
+ loader = Rails.autoloaders.main
15
+
16
+ if loader.respond_to?(:eager_load_dir)
17
+ loader.eager_load_dir(controllers_dir)
18
+ else
19
+ Dir.glob(File.join(controllers_dir, '**/*.rb')).each { |path| require path }
20
+ end
9
21
  end
10
22
  end
11
23
  end
@@ -6,7 +6,7 @@ module PlanMyStuff
6
6
  end
7
7
 
8
8
  # Raised when GitHub REST API returns a non-success HTTP status
9
- class APIError < Error
9
+ class APIError < PlanMyStuff::Error
10
10
  # @return [Integer]
11
11
  attr_reader :status
12
12
 
@@ -20,7 +20,7 @@ module PlanMyStuff
20
20
  end
21
21
 
22
22
  # Raised when GitHub GraphQL API returns errors in the response body
23
- class GraphQLError < Error
23
+ class GraphQLError < PlanMyStuff::Error
24
24
  # @return [Array<Hash>]
25
25
  attr_reader :errors
26
26
 
@@ -34,7 +34,7 @@ module PlanMyStuff
34
34
  end
35
35
 
36
36
  # Raised when GitHub rate limit is exhausted (429 or rate limit headers)
37
- class RateLimitError < Error
37
+ class RateLimitError < PlanMyStuff::Error
38
38
  # @return [Time]
39
39
  attr_reader :retry_after
40
40
 
@@ -48,7 +48,7 @@ module PlanMyStuff
48
48
  end
49
49
 
50
50
  # Raised when an object has been modified remotely since it was loaded
51
- class StaleObjectError < Error
51
+ class StaleObjectError < PlanMyStuff::Error
52
52
  # @return [Time, nil]
53
53
  attr_reader :local_updated_at
54
54
 
@@ -66,8 +66,19 @@ module PlanMyStuff
66
66
  end
67
67
  end
68
68
 
69
+ # Raised when a webhook HMAC-SHA256 signature check fails
70
+ class WebhookSignatureError < PlanMyStuff::Error
71
+ def initialize(message = 'Invalid webhook signature')
72
+ super
73
+ end
74
+ end
75
+
76
+ # Raised when a pipeline operation fails
77
+ class PipelineError < PlanMyStuff::Error
78
+ end
79
+
69
80
  # Raised when custom field validation fails (Phase 1)
70
- class ValidationError < Error
81
+ class ValidationError < PlanMyStuff::Error
71
82
  # @return [String, nil]
72
83
  attr_reader :field
73
84
 
@@ -84,4 +95,53 @@ module PlanMyStuff
84
95
  super(message)
85
96
  end
86
97
  end
98
+
99
+ # Raised when a caller is not authorized to perform an action (e.g.
100
+ # a non-support user attempts to manage approvers on an issue).
101
+ class AuthorizationError < PlanMyStuff::Error
102
+ end
103
+
104
+ # Raised when an operation is attempted on an issue whose conversation
105
+ # is locked on GitHub (archived or manually locked).
106
+ class LockedIssueError < PlanMyStuff::Error
107
+ end
108
+
109
+ # Raised when an Issue Fields API call is attempted while
110
+ # +config.issue_fields_enabled+ is +false+. Consumers whose org has not been
111
+ # admitted to the Issue Fields public preview flip the flag off; this error
112
+ # surfaces faster (and with a clearer message) than the underlying
113
+ # +GraphQLError+ that GitHub would otherwise return.
114
+ class IssueFieldsNotEnabledError < PlanMyStuff::Error
115
+ def initialize(message = nil)
116
+ super(message || 'Issue Fields are disabled; set config.issue_fields_enabled = true to enable')
117
+ end
118
+ end
119
+
120
+ # Raised by +PlanMyStuff::Pipeline+ forward transitions when the linked
121
+ # +Issue+ has any pending manager approvals.
122
+ class PendingApprovalsError < PlanMyStuff::ValidationError
123
+ # @return [PlanMyStuff::Issue, nil]
124
+ attr_reader :issue
125
+
126
+ # @return [Integer]
127
+ attr_reader :pending_count
128
+
129
+ # @param message [String, nil]
130
+ # @param issue [PlanMyStuff::Issue, nil]
131
+ # @param pending_count [Integer]
132
+ #
133
+ def initialize(message = nil, issue: nil, pending_count: 0)
134
+ @issue = issue
135
+ @pending_count = pending_count
136
+ super(message || default_message)
137
+ end
138
+
139
+ private
140
+
141
+ # @return [String]
142
+ def default_message
143
+ "Issue ##{issue&.number} has #{pending_count} pending approval(s); " \
144
+ 'cannot move forward through pipeline.'
145
+ end
146
+ end
87
147
  end