tefoji 3.2.0 → 3.4.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: 5b78e2ac6210a1adbd94b8ab0613522a8dbf024dccd1ff3e99321a554c7d7ba6
4
- data.tar.gz: 9aacc9f1a3735955f4b67cd4a804c379b3dfbd8e965fe8a27bca67c9d85ca2d6
3
+ metadata.gz: 69675edd7a74569645d7b4124adf46b6b89fe61b6b249e8773ce3a90e37d4cbd
4
+ data.tar.gz: 9925aa98c156453976e2e54b55231bd22dfb2de0fe9867ffb385d3030a3684a4
5
5
  SHA512:
6
- metadata.gz: e0522340f64d64008d536b3bd593a230c993b45097b846b9272b73f6e1b592dc56b420026f10b6c7294c3e597f21f411743ca3f330135a6465c6d9f107d7b7dc
7
- data.tar.gz: 457ca87866b1620062a968df3a0e733c40eb57402fec66848c0e243da51d8dc0da2888d3f7d0842f255c2d1fcbb0668cfefecfc295ae629977d8e1e541ed69ef
6
+ metadata.gz: fb28d7c19800cd8b61b8a30d44cb67f96c1dea95f31a40703d6fa74fe47f5865648dc433f6b757f853377fea76a3b7e053559753f6911f1ce7bdb553ea273cc3
7
+ data.tar.gz: 4ec1f5c99b091fd457482c9678c7d6b85fad114d3d212c126d1351214a0687bff5f94f19e40ebaefcc5a0298a5867fd67c70750e352c0d3556ee6966c771ebb1
@@ -14,6 +14,7 @@ module UserFunctions
14
14
  %w[
15
15
  chooser
16
16
  conditional
17
+ counter
17
18
  date_after
18
19
  date_before
19
20
  default
@@ -38,8 +39,8 @@ module UserFunctions
38
39
  CLIENT_TOOLS: 'CT',
39
40
  CODE_MANAGEMENT: 'CODEMGMT',
40
41
  COMMUNITY_PACKAGE_REPO: 'CPR',
41
- DOC: 'DOC',
42
- DOCS: 'DOC',
42
+ DOC: 'PUPDOC',
43
+ DOCS: 'PUPDOC',
43
44
  EZBAKE: 'EZ',
44
45
  FACTER: 'FACT',
45
46
  FORGE_INTERNAL: 'PF',
@@ -128,6 +129,24 @@ module UserFunctions
128
129
  end
129
130
  end
130
131
 
132
+ # Takes 2 optional arguments:
133
+ # initial_value (default 1), increment (default)
134
+ # Returns a Struct that starts with the initial value and post-increments every
135
+ # time it is used.
136
+ # The method name 'computed_value' is required for this form.
137
+ def counter(args)
138
+ initial_value = args[0] || 1
139
+ increment = args[1] || 1
140
+
141
+ Struct.new(:current_value, :increment) do
142
+ def computed_value
143
+ current = current_value.to_s
144
+ self.current_value += self.increment
145
+ current
146
+ end
147
+ end.new(initial_value, increment)
148
+ end
149
+
131
150
  # Date `number_of_days` after `date_string`
132
151
  def date_after(args)
133
152
  _fail_if_unset(__method__.to_s, args)
@@ -1,43 +1,60 @@
1
1
  module Tefoji
2
2
  class DeclaredValue
3
- attr_accessor :value, :properties
3
+ def initialize(user_value = nil)
4
+ @value = if user_value.is_a? DeclaredValue
5
+ # prevent recursive DeclaredValues
6
+ user_value.value
7
+ else
8
+ user_value
9
+ end
10
+ end
4
11
 
5
- def initialize(value = nil)
6
- @value = value
7
- @value = value.value if value.is_a? DeclaredValue
8
- @properties = {}
12
+ # For most DeclaredValues that aren't Structs that have their one `value` method
13
+ def simple?
14
+ !@value.respond_to?(:computed_value)
15
+ end
16
+
17
+ def value
18
+ if simple?
19
+ @value
20
+ else
21
+ @value.computed_value
22
+ end
9
23
  end
10
24
 
11
25
  def prepend(*prepended_things)
12
- @value.prepend(*prepended_things)
26
+ value.prepend(*prepended_things)
13
27
  end
14
28
 
15
29
  def to_i
16
- @value.to_i
30
+ value.to_i
17
31
  end
18
32
 
19
33
  def to_s
20
- @value.to_s
34
+ value.to_s
21
35
  end
22
36
 
23
37
  def to_a
24
- if @value.is_a? Array
25
- @value
38
+ current = value
39
+ if current.is_a? Array
40
+ current
26
41
  else
27
- [@value]
42
+ [current]
28
43
  end
29
44
  end
30
45
 
31
46
  def %(other)
32
- @value % other
47
+ value % other
33
48
  end
34
49
 
50
+ # When querying @value, we don't want to invoke @value.computed_value because it may have
51
+ # unwanted side-effects.
35
52
  def unset?
36
53
  @value.nil?
37
54
  end
38
55
 
39
56
  def falsey?
40
- @value.nil? || @value.empty? || @value.downcase == 'false' || @value == false
57
+ simple? && (@value.nil? || @value.empty? || @value.downcase == 'false' || @value == false)
41
58
  end
42
59
 
43
60
  def truthy?
@@ -4,8 +4,6 @@ require 'json'
4
4
  require 'rest-client'
5
5
  require 'uri'
6
6
 
7
- RestClient.log = 'stdout'
8
-
9
7
  module Tefoji
10
8
  ## An interface to send API request to Jira using the rest-client gem.
11
9
  ## Contained here are the bits of knowledge necessary to form API calls to Jira.
@@ -32,7 +30,7 @@ module Tefoji
32
30
  def log_level=(level)
33
31
  @log_level = level
34
32
  # When debugging, turn on the RestClient @logger
35
- RestClient.log = @logger if @log_level == Logger::DEBUG
33
+ RestClient.log = 'stderr' if @log_level == Logger::DEBUG
36
34
  end
37
35
 
38
36
  # Depending on how user specified their authorization we want to
@@ -92,6 +90,12 @@ module Tefoji
92
90
  jira_get(search_parameters, fail_if_not_found)
93
91
  end
94
92
 
93
+ # Get information about user in Jira
94
+ def get_components(project)
95
+ search_parameters = "project/#{project}/components"
96
+ jira_get(search_parameters)
97
+ end
98
+
95
99
  # Save authentication YAML to the a file for reuse.
96
100
  def save_authentication(save_file_name)
97
101
  backup_file_name = "#{save_file_name}.bak"
@@ -0,0 +1,3 @@
1
+ module Tefoji
2
+ VERSION = '3.4.0'
3
+ end
data/lib/tefoji.rb CHANGED
@@ -43,6 +43,7 @@ module Tefoji
43
43
  @epic_security = nil
44
44
  @deferral_data = {}
45
45
  @transitions_table = {}
46
+ @valid_components = {}
46
47
 
47
48
  # Logging
48
49
  @log_level = Logger::INFO
@@ -78,6 +79,7 @@ module Tefoji
78
79
  @logger.debug "Declarations: #{@declarations}"
79
80
 
80
81
  check_assignees(main_template_data)
82
+ check_components(main_template_data)
81
83
 
82
84
  # Process 'before' templates
83
85
  main_template.dig(:includes, :before)&.each do |before|
@@ -506,7 +508,7 @@ module Tefoji
506
508
  .gsub('%{short_name}', raw_issue_data['short_name'].value)
507
509
 
508
510
  # Now do generic expansion of anything else
509
- DeclaredValue.new(expand_string(formatted))
511
+ DeclaredValue.new(expand_variables_in_string(formatted))
510
512
  end
511
513
 
512
514
  # These tags must not be in the issue creation because they need to be
@@ -629,25 +631,23 @@ module Tefoji
629
631
  def user_function_call(function_definition)
630
632
  @logger.debug "function_definition is: #{function_definition}"
631
633
  function_name = function_definition['name']
632
- argument = function_definition['argument']
633
- arguments = function_definition['arguments']
634
-
635
- if argument.is_a?(Array) || arguments.is_a?(Array)
636
- arguments = argument || function_definition['arguments']
637
- function_arguments = arguments.map do |a|
638
- expand_right_value(a).value
639
- end
640
- elsif argument.is_a?(String) || arguments.is_a?(String)
641
- argument = function_definition['argument'] || function_definition['arguments']
642
- function_arguments = [expand_right_value(argument).value]
643
- else
644
- fatal("No arguments supplied to function \"#{function_name}\"")
645
- end
634
+ # Allow singlular or plural form for any function call
635
+ user_arguments = function_definition['argument'] || function_definition['arguments']
646
636
 
647
637
  unless user_function_allowlist.include?(function_name)
648
638
  fatal "Unknown function call: \"#{function_name}\""
649
639
  end
650
640
 
641
+ function_arguments = if user_arguments.is_a?(Array)
642
+ user_arguments.map do |a|
643
+ expand_right_value(a).value
644
+ end
645
+ elsif user_arguments.is_a?(String)
646
+ [expand_right_value(user_arguments).value]
647
+ else
648
+ []
649
+ end
650
+
651
651
  return_value = send(function_name, function_arguments)
652
652
  @logger.debug "send(#{function_name}, #{function_arguments}) => #{return_value}"
653
653
  DeclaredValue.new(return_value)
@@ -695,11 +695,11 @@ module Tefoji
695
695
  # Some simple cases
696
696
  return value if value.is_a?(DeclaredValue)
697
697
  return DeclaredValue.new(value) if value.is_a?(Numeric)
698
- return DeclaredValue.new(expand_string(value)) if value.is_a?(String)
698
+ return DeclaredValue.new(expand_variables_in_string(value)) if value.is_a?(String)
699
699
 
700
700
  # Do a user-function call
701
701
  if value.is_a?(Hash) && value.key?('function')
702
- return DeclaredValue.new(user_function_call(value['function']))
702
+ return user_function_call(value['function'])
703
703
  end
704
704
 
705
705
  # Return the original hash with the values variable-expanded
@@ -726,13 +726,39 @@ module Tefoji
726
726
  raise "Unimplemented condition in expand_right_value: (#{value.class}) #{value}"
727
727
  end
728
728
 
729
- def expand_string(value)
729
+ def expand_variables_in_string(expandable_string)
730
+ # If there are no symbols, there's nothing to expand
731
+ return expandable_string if @declarations.nil?
732
+
730
733
  # Special-case where 'foo: "%{bar}"'; this allows for passing around hashes and
731
734
  # arrays without interpolation into strings
732
- single_value = %r{\A%{(.+?)}\Z}.match(value)
735
+ single_value = %r/\A%{([^}]+)}\Z/.match(expandable_string)
733
736
  return @declarations[single_value[1].to_sym] if single_value
734
737
 
735
- return value % @declarations
738
+ expand_variables(expandable_string)
739
+ end
740
+
741
+ # Generic form of #expand_variables_in_string that recursively replaces
742
+ # variable forms ("%{foo}") in a string with the text of the variable from
743
+ # @declarations
744
+ def expand_variables(expandable_string)
745
+ # Any pattern starting with '%{', followed by 1 or more characters, ending with '}'
746
+ variable_match_data = %r/%{([^}]+)}/.match(expandable_string)
747
+
748
+ # Return the string if there's nothing to expand
749
+ return expandable_string if variable_match_data.nil?
750
+
751
+ variable_expression = variable_match_data[0]
752
+ variable_symbol = variable_match_data[1].to_sym
753
+ variable_value = @declarations[variable_symbol].value
754
+ variable_stringified_value = variable_value.to_s
755
+
756
+ if variable_value.nil?
757
+ fatal "Unknown variable: \"#{variable_expression}\""
758
+ end
759
+
760
+ modified_string = expandable_string.sub(variable_expression, variable_stringified_value)
761
+ expand_variables(modified_string)
736
762
  end
737
763
 
738
764
  # Return a list of all the unset variables in a template
@@ -816,7 +842,6 @@ module Tefoji
816
842
  # reasonable way to check assignability prior to the
817
843
  # epic/issue being created, we should implement it in place
818
844
  # of using the get_username method.
819
-
820
845
  def check_assignees(main_template_data)
821
846
  valid_users = []
822
847
  invalid_users_to_epics = {}
@@ -897,7 +922,7 @@ module Tefoji
897
922
 
898
923
  def get_username_from_assignee(assignee)
899
924
  # Assignee is a variable or direct input
900
- username = expand_string(assignee) if assignee.is_a?(String)
925
+ username = expand_variables_in_string(assignee) if assignee.is_a?(String)
901
926
  # Assignee is a function
902
927
  if assignee.is_a?(Hash) && assignee.key?('function')
903
928
  username = user_function_call(assignee['function'])
@@ -922,6 +947,57 @@ module Tefoji
922
947
  end
923
948
  end
924
949
 
950
+ # Takes a Tefoji template as input and verifies that all components used in the template exist
951
+ # in the associated project.
952
+ def check_components(main_template_data)
953
+ invalid_components = Set.new
954
+ main_template_data['epics']&.each do |epic|
955
+ next unless epic['components']
956
+
957
+ project = expand_right_value(epic['project']).value
958
+ update_valid_components(project)
959
+ epic['components'].each do |component|
960
+ invalid_components.add([project, component]) unless @valid_components[project].include?(component)
961
+ end
962
+ end
963
+
964
+ if main_template_data.dig('epic', 'components')
965
+ project = expand_right_value(main_template_data['epic']['project']).value
966
+ update_valid_components(project)
967
+ main_template_data['epic']['components'].each do |component|
968
+ invalid_components.add([project, component]) unless @valid_components[project].include?(component)
969
+ end
970
+ end
971
+
972
+ main_template_data['issues'].each do |issue|
973
+ next unless issue['components']
974
+
975
+ project = if issue['project']
976
+ expand_right_value(issue['project']).value
977
+ else
978
+ expand_right_value(main_template_data['issue_defaults']['project']).value
979
+ end
980
+ update_valid_components(project)
981
+ issue['components'].each do |component|
982
+ invalid_components.add([project, component]) unless @valid_components[project].include?(component)
983
+ end
984
+ end
985
+
986
+ fail_message = "component validation failed. the following components do not exist within the given projects: \n"
987
+ invalid_components.each do |component_project, component|
988
+ fail_message += "#{component_project}, #{component} \n"
989
+ end
990
+ fatal fail_message unless invalid_components.empty?
991
+ end
992
+
993
+ def update_valid_components(project)
994
+ return if @valid_components.key?(project)
995
+
996
+ components_response = @jira_api.get_components(project)
997
+ valid_components = components_response.map { |component_data| component_data['name'] }
998
+ @valid_components[project] = valid_components
999
+ end
1000
+
925
1001
  def logger_initialize(log_location)
926
1002
  logger = Logger.new(log_location, progname: 'tefoji', level: @log_level)
927
1003
  logger.formatter = proc do |severity, _, script_name, message|
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tefoji
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.2.0
4
+ version: 3.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Puppet By Perforce
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-10-12 00:00:00.000000000 Z
11
+ date: 2024-01-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: debug
@@ -153,7 +153,7 @@ dependencies:
153
153
  description: 'From a YAML specification, create a batch of Jira issues allowing for
154
154
  variable substitutions.
155
155
 
156
- '
156
+ '
157
157
  email: release@puppet.com
158
158
  executables:
159
159
  - tefoji
@@ -167,6 +167,7 @@ files:
167
167
  - lib/tefoji/cli.rb
168
168
  - lib/tefoji/declared_value.rb
169
169
  - lib/tefoji/jira_api.rb
170
+ - lib/tefoji/version.rb
170
171
  homepage: https://github.com/puppetlabs/tefoji
171
172
  licenses:
172
173
  - Apache-2.0
@@ -189,7 +190,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
189
190
  - !ruby/object:Gem::Version
190
191
  version: '0'
191
192
  requirements: []
192
- rubygems_version: 3.0.3
193
+ rubygems_version: 3.2.33
193
194
  signing_key:
194
195
  specification_version: 4
195
196
  summary: Generate Jira issues from a YAML specification.