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
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'net/http'
5
+ require 'securerandom'
6
+ require 'time'
7
+ require 'uri'
8
+
9
+ module PlanMyStuff
10
+ # Dev helper: build a fake SNS notification wrapping an ECS Deployment
11
+ # State Change event and POST it to a local +/webhooks/aws+ endpoint.
12
+ # Used by +plan_my_stuff:webhooks:simulate_aws+ so deployment flows
13
+ # can be exercised without waiting for a real AWS deployment.
14
+ #
15
+ # The consuming app must be configured with a no-op SNS verifier
16
+ # (see +PlanMyStuff::NullSnsVerifier+) or signature verification will
17
+ # reject the simulated envelope. Never enable that verifier in
18
+ # production.
19
+ #
20
+ module AwsSnsSimulator
21
+ DEFAULT_EVENT = 'SERVICE_DEPLOYMENT_COMPLETED'
22
+
23
+ module_function
24
+
25
+ # POSTs a simulated SNS envelope to the endpoint. Pulls
26
+ # +sns_topic_arn+, +aws_service_identifier+, and
27
+ # +production_commit_sha+ from +PlanMyStuff.configuration+ (this
28
+ # task is dev-only, so there's no reason to parameterize them).
29
+ #
30
+ # @raise [PlanMyStuff::ConfigurationError] if sns details are missing
31
+ #
32
+ # @param endpoint_url [String] full URL including path
33
+ # @param event_name [String] ECS eventName (default completed)
34
+ #
35
+ # @return [Net::HTTPResponse]
36
+ #
37
+ def post!(endpoint_url:, event_name: DEFAULT_EVENT)
38
+ config = PlanMyStuff.configuration
39
+ if config.sns_topic_arn.blank?
40
+ raise(PlanMyStuff::ConfigurationError, 'PlanMyStuff.configuration.sns_topic_arn is blank')
41
+ end
42
+ if config.aws_service_identifier.blank?
43
+ raise(PlanMyStuff::ConfigurationError, 'PlanMyStuff.configuration.aws_service_identifier is blank')
44
+ end
45
+
46
+ topic_arn = config.sns_topic_arn
47
+ service_arn = "arn:aws:ecs:us-east-1:000000000000:service/simulated-cluster/#{config.aws_service_identifier}"
48
+ commit_sha = config.production_commit_sha
49
+
50
+ message = build_ecs_message(event_name: event_name, service_arn: service_arn, commit_sha: commit_sha)
51
+ envelope = build_sns_envelope(message: message, topic_arn: topic_arn)
52
+ raw_body = JSON.generate(envelope)
53
+
54
+ uri = URI(endpoint_url)
55
+ http = Net::HTTP.new(uri.host, uri.port)
56
+ http.use_ssl = uri.scheme == 'https'
57
+
58
+ request = Net::HTTP::Post.new(uri.request_uri)
59
+ request['Content-Type'] = 'application/json'
60
+ request['x-amz-sns-message-type'] = 'Notification'
61
+ request.body = raw_body
62
+
63
+ $stdout.puts("POST #{endpoint_url}")
64
+ $stdout.puts(" event: #{event_name}")
65
+ $stdout.puts(" topic: #{topic_arn}")
66
+ $stdout.puts(" resources: #{service_arn}")
67
+ $stdout.puts(" commit: #{commit_sha || '(none)'}")
68
+ $stdout.puts('---')
69
+
70
+ response = http.request(request)
71
+ $stdout.puts("HTTP #{response.code} #{response.message}")
72
+ $stdout.puts(response.body) if response.body.present?
73
+ response
74
+ end
75
+
76
+ # @return [Hash]
77
+ def build_ecs_message(event_name:, service_arn:, commit_sha:)
78
+ detail = {
79
+ 'eventType' => 'INFO',
80
+ 'eventName' => event_name,
81
+ 'deploymentId' => "ecs-svc/#{SecureRandom.hex(8)}",
82
+ 'updatedAt' => PlanMyStuff.format_time(Time.now.utc),
83
+ }
84
+ detail['commitSha'] = commit_sha if commit_sha
85
+
86
+ {
87
+ 'version' => '0',
88
+ 'id' => SecureRandom.uuid,
89
+ 'detail-type' => 'ECS Deployment State Change',
90
+ 'source' => 'aws.ecs',
91
+ 'account' => '000000000000',
92
+ 'time' => PlanMyStuff.format_time(Time.now.utc),
93
+ 'region' => 'us-east-1',
94
+ 'resources' => [service_arn],
95
+ 'detail' => detail,
96
+ }
97
+ end
98
+
99
+ # @return [Hash]
100
+ def build_sns_envelope(message:, topic_arn:)
101
+ now = PlanMyStuff.format_time(Time.now.utc)
102
+ {
103
+ 'Type' => 'Notification',
104
+ 'MessageId' => SecureRandom.uuid,
105
+ 'TopicArn' => topic_arn,
106
+ 'Subject' => 'ECS Deployment State Change',
107
+ 'Message' => JSON.generate(message),
108
+ 'Timestamp' => now,
109
+ 'SignatureVersion' => '1',
110
+ 'Signature' => 'simulated',
111
+ 'SigningCertURL' => 'https://example.com/cert.pem',
112
+ 'UnsubscribeURL' => 'https://example.com/unsubscribe',
113
+ }
114
+ end
115
+ end
116
+ end
@@ -15,10 +15,6 @@ module PlanMyStuff
15
15
  attr_accessor :rails_env
16
16
  # @return [String, nil] consuming app name from config
17
17
  attr_accessor :app_name
18
- # @return [Time, nil] timestamp of creation
19
- attr_accessor :created_at
20
- # @return [Time, nil] timestamp of last update
21
- attr_accessor :updated_at
22
18
  # @return [Integer, nil] consuming app's user ID of the creator
23
19
  attr_accessor :created_by
24
20
  # @return [String] "public" or "internal"
@@ -33,20 +29,19 @@ module PlanMyStuff
33
29
  #
34
30
  # @param metadata [BaseMetadata]
35
31
  # @param hash [Hash]
32
+ # @param custom_fields_schema [Hash{Symbol => Hash}] merged schema for this context
36
33
  #
37
34
  # @return [void]
38
35
  #
39
- def apply_common_from_hash(metadata, hash)
36
+ def apply_common_from_hash(metadata, hash, custom_fields_schema)
40
37
  metadata.schema_version = hash[:schema_version]
41
38
  metadata.gem_version = hash[:gem_version]
42
39
  metadata.rails_env = hash[:rails_env]
43
40
  metadata.app_name = hash[:app_name]
44
- metadata.created_at = parse_time(hash[:created_at])
45
- metadata.updated_at = parse_time(hash[:updated_at])
46
41
  metadata.created_by = hash[:created_by]
47
42
  metadata.visibility = hash.fetch(:visibility, 'internal')
48
- metadata.custom_fields = CustomFields.new(
49
- PlanMyStuff.configuration.custom_fields,
43
+ metadata.custom_fields = PlanMyStuff::CustomFields.new(
44
+ custom_fields_schema,
50
45
  hash[:custom_fields] || {},
51
46
  )
52
47
  end
@@ -57,24 +52,28 @@ module PlanMyStuff
57
52
  # @param user [Object, Integer] user object or user_id
58
53
  # @param visibility [String] "public" or "internal"
59
54
  # @param custom_fields_data [Hash]
55
+ # @param custom_fields_schema [Hash{Symbol => Hash}] merged schema for this context
60
56
  #
61
57
  # @return [void]
62
58
  #
63
- def apply_common_build(metadata, user:, visibility: 'internal', custom_fields_data: {})
59
+ def apply_common_build(
60
+ metadata,
61
+ user:,
62
+ visibility: 'internal',
63
+ custom_fields_data: {},
64
+ custom_fields_schema: {}
65
+ )
64
66
  config = PlanMyStuff.configuration
65
- now = Time.now.utc
66
67
 
67
68
  metadata.schema_version = self::SCHEMA_VERSION
68
69
  metadata.gem_version = PlanMyStuff::VERSION::STRING
69
70
  metadata.rails_env = (defined?(Rails) && Rails.respond_to?(:env)) ? Rails.env.to_s : nil
70
71
  metadata.app_name = config.app_name
71
- metadata.created_at = now
72
- metadata.updated_at = now
73
- resolved = UserResolver.resolve(user)
74
- metadata.created_by = resolved.present? ? UserResolver.user_id(resolved) : nil
72
+ resolved = PlanMyStuff::UserResolver.resolve(user)
73
+ metadata.created_by = resolved.present? ? PlanMyStuff::UserResolver.user_id(resolved) : nil
75
74
  metadata.visibility = visibility
76
- metadata.custom_fields = CustomFields.new(
77
- config.custom_fields,
75
+ metadata.custom_fields = PlanMyStuff::CustomFields.new(
76
+ custom_fields_schema,
78
77
  custom_fields_data,
79
78
  )
80
79
  end
@@ -92,6 +91,7 @@ module PlanMyStuff
92
91
 
93
92
  def initialize
94
93
  @visibility = 'internal'
94
+ @custom_fields = {}
95
95
  end
96
96
 
97
97
  # @return [Hash]
@@ -101,8 +101,6 @@ module PlanMyStuff
101
101
  gem_version: gem_version,
102
102
  rails_env: rails_env,
103
103
  app_name: app_name,
104
- created_at: format_time(created_at),
105
- updated_at: format_time(updated_at),
106
104
  created_by: created_by,
107
105
  visibility: visibility,
108
106
  custom_fields: custom_fields.to_h,
@@ -119,18 +117,17 @@ module PlanMyStuff
119
117
  visibility == 'public'
120
118
  end
121
119
 
120
+ # Validates custom fields against the schema.
121
+ #
122
+ # @return [true]
123
+ #
124
+ def validate_custom_fields!
125
+ custom_fields.validate! if custom_fields.is_a?(PlanMyStuff::CustomFields)
126
+ end
127
+
122
128
  # @return [String]
123
129
  def to_json(...)
124
130
  to_h.to_json(...)
125
131
  end
126
-
127
- private
128
-
129
- # @return [String, nil]
130
- def format_time(time)
131
- return if time.nil?
132
-
133
- time.utc.iso8601
134
- end
135
132
  end
136
133
  end