feature_map 1.1.1 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 206bf1a9f4dcf4bb82658ce1a6e7f3c532b5929521e53c070b4efa92eb4b61b1
4
- data.tar.gz: dbde9980709b0d3a28bb2b7319da42a9bbd188aec2c78c9ecbaef6405a95bf96
3
+ metadata.gz: 1d557f6ea527d73672a53ea6a279e8574a0157d03ffe5da289cc12aa05fec2d3
4
+ data.tar.gz: 0b5d04be8cfd69f16d8f5ffc8e1ef0b4c7e5fa8de276feb3c776c7d733cce758
5
5
  SHA512:
6
- metadata.gz: 03b1baec50bb36335af87eabf62885290cfe53516686bded3ac61d6ae660dd9d181d6df817e1b2089f0e3c605dfdf119bedd334d61ae0c9ee1049fe29ee7cd75
7
- data.tar.gz: c2c72319780a7043aa1654cf691d53a576275af18ef968fde2777a13b89bba4208c77daed041ff0def4ec15f0851ce1cc9bdf3c4d724e89af28a7c26cd41a8be
6
+ metadata.gz: 5fa1edc5ba79772619aec3f730147ccffa37a19cc889959192086469757522d72f4c635fa67cb815237c9b0a8e71557c2fb52948e9f9db63b4c1f4338f0ca316
7
+ data.tar.gz: 7d75f638f6d06921b146d81a590bebb72e9db3ed1229bf833781aca68b9e92a274fb016190af9884ba8800c6ca982945a370634e73af3b9171b0c873c73cfcdc
@@ -0,0 +1,34 @@
1
+ # typed: strict
2
+
3
+ module FeatureMap
4
+ class Commit
5
+ extend T::Sig
6
+
7
+ sig { returns(T.nilable(String)) }
8
+ attr_reader :sha
9
+
10
+ sig { returns(T.nilable(String)) }
11
+ attr_reader :description
12
+
13
+ sig { returns(T.nilable(String)) }
14
+ attr_reader :pull_request_number
15
+
16
+ sig { returns(T::Array[String]) }
17
+ attr_reader :files
18
+
19
+ sig do
20
+ params(
21
+ sha: T.nilable(String),
22
+ description: T.nilable(String),
23
+ pull_request_number: T.nilable(String),
24
+ files: T::Array[String]
25
+ ).void
26
+ end
27
+ def initialize(sha: nil, description: nil, pull_request_number: nil, files: [])
28
+ @sha = sha
29
+ @description = description
30
+ @pull_request_number = pull_request_number
31
+ @files = files
32
+ end
33
+ end
34
+ end
@@ -15,6 +15,7 @@ module FeatureMap
15
15
  const :code_cov, T::Hash[String, T.nilable(String)]
16
16
  const :repository, T::Hash[String, T.nilable(String)]
17
17
  const :documentation_site, T::Hash[String, T.untyped]
18
+ const :documentation_site_url, T.nilable(String)
18
19
 
19
20
  sig { returns(Configuration) }
20
21
  def self.fetch
@@ -36,7 +37,8 @@ module FeatureMap
36
37
  ignore_feature_definitions: config_hash.fetch('ignore_feature_definitions', false),
37
38
  code_cov: config_hash.fetch('code_cov', {}),
38
39
  repository: config_hash.fetch('repository', {}),
39
- documentation_site: config_hash.fetch('documentation_site', {})
40
+ documentation_site: config_hash.fetch('documentation_site', {}),
41
+ documentation_site_url: config_hash.fetch('documentation_site_url', nil)
40
42
  )
41
43
  end
42
44
  end
@@ -1,6 +1,8 @@
1
1
  # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
+ require 'code_ownership'
5
+
4
6
  module FeatureMap
5
7
  module Private
6
8
  #
@@ -0,0 +1,76 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module FeatureMap
5
+ module Private
6
+ #
7
+ # This class is responsible for building a release notification message that can be published to a single
8
+ # team or to an organization as a whole. Currently, the only supported output format is Slack's Block Kit
9
+ # (see https://app.slack.com/block-kit-builder).
10
+ #
11
+ class ReleaseNotificationBuilder
12
+ extend T::Sig
13
+
14
+ BlockKitSection = T.type_alias { T::Hash[String, T.untyped] }
15
+ BlockKitPayload = T.type_alias { T::Array[T::Hash[String, T.untyped]] }
16
+
17
+ class << self
18
+ extend T::Sig
19
+
20
+ sig { params(commits_by_feature: CommitsByFeature).returns(BlockKitPayload) }
21
+ def build(commits_by_feature)
22
+ return [] if commits_by_feature.empty?
23
+
24
+ feature_names = commits_by_feature.keys.sort
25
+
26
+ feature_names.flat_map.with_index do |feature_name, index|
27
+ # Insert a divider between each feature but not above the first feature nor below the last feature.
28
+ divider = index.zero? ? [] : [{ type: 'divider' }]
29
+ divider + [build_feature_section(feature_name, T.must(commits_by_feature[feature_name]))]
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ sig { params(feature_name: String, commits: T::Array[Commit]).returns(BlockKitSection) }
36
+ def build_feature_section(feature_name, commits)
37
+ feature_header_markdown = "*_#{feature_name}_*"
38
+
39
+ # If the docs site is hosted at a persistent URL, include a link to the feature show page.
40
+ if documentation_site_url && feature_name != FeatureMap::NO_FEATURE_KEY
41
+ feature_header_markdown += " (<#{documentation_site_url}#/#{URI::DEFAULT_PARSER.escape(feature_name)}|View Documentation>)"
42
+ end
43
+
44
+ feature_markdown_lines = [feature_header_markdown]
45
+ commits.each do |commit|
46
+ commit_sub_bullet = "• #{commit.description}"
47
+
48
+ if repository_url && commit.pull_request_number
49
+ commit_sub_bullet += " (<#{repository_url}/pull/#{commit.pull_request_number}|##{commit.pull_request_number}>)"
50
+ end
51
+
52
+ feature_markdown_lines << commit_sub_bullet
53
+ end
54
+
55
+ {
56
+ type: 'section',
57
+ text: {
58
+ type: 'mrkdwn',
59
+ text: feature_markdown_lines.join("\n")
60
+ }
61
+ }
62
+ end
63
+
64
+ sig { returns(T.nilable(String)) }
65
+ def documentation_site_url
66
+ Private.configuration.documentation_site_url
67
+ end
68
+
69
+ sig { returns(T.nilable(String)) }
70
+ def repository_url
71
+ Private.configuration.repository['url']
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -2,6 +2,7 @@
2
2
 
3
3
  # typed: strict
4
4
 
5
+ require 'code_ownership'
5
6
  require 'csv'
6
7
 
7
8
  require 'feature_map/constants'
@@ -27,6 +28,7 @@ require 'feature_map/private/assignment_mappers/file_annotations'
27
28
  require 'feature_map/private/assignment_mappers/feature_globs'
28
29
  require 'feature_map/private/assignment_mappers/directory_assignment'
29
30
  require 'feature_map/private/assignment_mappers/feature_definition_assignment'
31
+ require 'feature_map/private/release_notification_builder'
30
32
 
31
33
  module FeatureMap
32
34
  module Private
@@ -207,6 +209,23 @@ module FeatureMap
207
209
  feature_files
208
210
  end
209
211
  end
212
+
213
+ sig { params(feature: CodeFeatures::Feature).returns(T::Array[CodeTeams::Team]) }
214
+ def self.all_teams_for_feature(feature)
215
+ return [] if configuration.skip_code_ownership
216
+
217
+ feature_assignments = AssignmentsFile.load_features!
218
+
219
+ feature_files = feature_assignments.dig(feature.name, AssignmentsFile::FILES_KEY)
220
+ return [] if !feature_files || feature_files.empty?
221
+
222
+ feature_files.map { |file| CodeOwnership.for_file(file) }.compact.uniq
223
+ end
224
+
225
+ sig { params(commits_by_feature: CommitsByFeature).returns(ReleaseNotificationBuilder::BlockKitPayload) }
226
+ def self.generate_release_notification(commits_by_feature)
227
+ ReleaseNotificationBuilder.build(commits_by_feature)
228
+ end
210
229
  end
211
230
 
212
231
  private_constant :Private
data/lib/feature_map.rb CHANGED
@@ -6,6 +6,7 @@ require 'set'
6
6
  require 'sorbet-runtime'
7
7
  require 'json'
8
8
  require 'yaml'
9
+ require 'feature_map/commit'
9
10
  require 'feature_map/code_features'
10
11
  require 'feature_map/mapper'
11
12
  require 'feature_map/validator'
@@ -14,6 +15,9 @@ require 'feature_map/cli'
14
15
  require 'feature_map/configuration'
15
16
 
16
17
  module FeatureMap
18
+ ALL_TEAMS_KEY = 'All Teams'
19
+ NO_FEATURE_KEY = 'No Feature'
20
+
17
21
  module_function
18
22
 
19
23
  extend T::Sig
@@ -22,6 +26,9 @@ module FeatureMap
22
26
  requires_ancestor { Kernel }
23
27
  GlobsToAssignedFeatureMap = T.type_alias { T::Hash[String, CodeFeatures::Feature] }
24
28
 
29
+ UpdatedFeaturesByTeam = T.type_alias { T::Hash[String, CommitsByFeature] }
30
+ CommitsByFeature = T.type_alias { T::Hash[String, T::Array[Commit]] }
31
+
25
32
  sig { params(assignments_file_path: String).void }
26
33
  def apply_assignments!(assignments_file_path)
27
34
  Private.apply_assignments!(assignments_file_path)
@@ -198,6 +205,45 @@ module FeatureMap
198
205
  end
199
206
  end
200
207
 
208
+ # Groups the provided list of commits (e.g. the changes being deployed in a release) by both the feature they impact
209
+ # and the teams responsible for these features. Returns a hash with keys for each team with features modified within
210
+ # these commits and values that are a hash of features to the set of commits that impact each feature.
211
+ sig { params(commits: T::Array[Commit]).returns(UpdatedFeaturesByTeam) }
212
+ def group_commits(commits)
213
+ commits.each_with_object({}) do |commit, hash|
214
+ commit_features = commit.files.map do |file|
215
+ feature = FeatureMap.for_file(file)
216
+ next nil unless feature
217
+
218
+ teams = Private.all_teams_for_feature(feature)
219
+ team_names = teams.empty? ? [ALL_TEAMS_KEY] : teams.map(&:name)
220
+
221
+ team_names.sort.each do |team_name|
222
+ hash[team_name] ||= {}
223
+ hash[team_name][feature.name] ||= []
224
+ hash[team_name][feature.name] << commit unless hash[team_name][feature.name].include?(commit)
225
+ end
226
+
227
+ feature
228
+ end
229
+
230
+ # If the commit did not have any files that relate to a specific feature, include it in a "No Feature" section
231
+ # of the "All Teams" grouping to avoid it being omitted from the resulting grouped commits entirely.
232
+ next unless commit_features.compact.empty?
233
+
234
+ hash[ALL_TEAMS_KEY] ||= {}
235
+ hash[ALL_TEAMS_KEY][NO_FEATURE_KEY] ||= []
236
+ hash[ALL_TEAMS_KEY][NO_FEATURE_KEY] << commit
237
+ end
238
+ end
239
+
240
+ # Generates a block kit message grouping the provided commits into sections for each feature impacted by the
241
+ # cheanges.
242
+ sig { params(commits_by_feature: CommitsByFeature).returns(T::Array[T::Hash[String, T.untyped]]) }
243
+ def generate_release_notification(commits_by_feature)
244
+ Private.generate_release_notification(commits_by_feature)
245
+ end
246
+
201
247
  # Generally, you should not ever need to do this, because once your ruby process loads, cached content should not change.
202
248
  # Namely, the set of files, and directories which are tracked for feature assignment should not change.
203
249
  # The primary reason this is helpful is for clients of FeatureMap who want to test their code, and each test context
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: feature_map
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.1
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Beyond Finance
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-02-04 00:00:00.000000000 Z
11
+ date: 2025-02-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: code_ownership
@@ -80,6 +80,20 @@ dependencies:
80
80
  - - "~>"
81
81
  - !ruby/object:Gem::Version
82
82
  version: '0.5'
83
+ - !ruby/object:Gem::Dependency
84
+ name: uri
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '1.0'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '1.0'
83
97
  - !ruby/object:Gem::Dependency
84
98
  name: debug
85
99
  requirement: !ruby/object:Gem::Requirement
@@ -195,6 +209,7 @@ files:
195
209
  - lib/feature_map/code_features.rb
196
210
  - lib/feature_map/code_features/plugin.rb
197
211
  - lib/feature_map/code_features/plugins/identity.rb
212
+ - lib/feature_map/commit.rb
198
213
  - lib/feature_map/configuration.rb
199
214
  - lib/feature_map/constants.rb
200
215
  - lib/feature_map/mapper.rb
@@ -217,6 +232,7 @@ files:
217
232
  - lib/feature_map/private/glob_cache.rb
218
233
  - lib/feature_map/private/lines_of_code_calculator.rb
219
234
  - lib/feature_map/private/metrics_file.rb
235
+ - lib/feature_map/private/release_notification_builder.rb
220
236
  - lib/feature_map/private/test_coverage_file.rb
221
237
  - lib/feature_map/private/test_pyramid_file.rb
222
238
  - lib/feature_map/private/todo_inspector.rb