feature_map 1.1.1 → 1.2.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 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