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 +4 -4
- data/lib/feature_map/commit.rb +34 -0
- data/lib/feature_map/configuration.rb +3 -1
- data/lib/feature_map/private/assignments_file.rb +2 -0
- data/lib/feature_map/private/release_notification_builder.rb +76 -0
- data/lib/feature_map/private.rb +19 -0
- data/lib/feature_map.rb +46 -0
- metadata +18 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1d557f6ea527d73672a53ea6a279e8574a0157d03ffe5da289cc12aa05fec2d3
|
4
|
+
data.tar.gz: 0b5d04be8cfd69f16d8f5ffc8e1ef0b4c7e5fa8de276feb3c776c7d733cce758
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
@@ -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
|
data/lib/feature_map/private.rb
CHANGED
@@ -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.
|
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-
|
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
|