releasehx 0.1.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 (91) hide show
  1. checksums.yaml +7 -0
  2. data/README.adoc +2915 -0
  3. data/bin/releasehx +7 -0
  4. data/bin/rhx +7 -0
  5. data/bin/rhx-mcp +7 -0
  6. data/bin/sourcerer +32 -0
  7. data/build/docs/CNAME +1 -0
  8. data/build/docs/Gemfile.lock +95 -0
  9. data/build/docs/_config.yml +36 -0
  10. data/build/docs/config-reference.adoc +4104 -0
  11. data/build/docs/config-reference.json +1546 -0
  12. data/build/docs/index.adoc +2915 -0
  13. data/build/docs/landing.adoc +21 -0
  14. data/build/docs/manpage.adoc +68 -0
  15. data/build/docs/releasehx.1 +281 -0
  16. data/build/docs/releasehx_readme.html +367 -0
  17. data/build/docs/sample-config.adoc +9 -0
  18. data/build/docs/sample-config.yml +251 -0
  19. data/build/docs/schemagraphy_readme.html +0 -0
  20. data/build/docs/sourcerer_readme.html +46 -0
  21. data/build/snippets/helpscreen.txt +29 -0
  22. data/lib/docopslab/mcp/asset_packager.rb +30 -0
  23. data/lib/docopslab/mcp/manifest.rb +67 -0
  24. data/lib/docopslab/mcp/resource_pack.rb +46 -0
  25. data/lib/docopslab/mcp/server.rb +92 -0
  26. data/lib/docopslab/mcp.rb +6 -0
  27. data/lib/releasehx/cli.rb +937 -0
  28. data/lib/releasehx/configuration.rb +215 -0
  29. data/lib/releasehx/generated.rb +17 -0
  30. data/lib/releasehx/helpers.rb +58 -0
  31. data/lib/releasehx/mcp/asset_packager.rb +21 -0
  32. data/lib/releasehx/mcp/assets/agent-config-guide.md +178 -0
  33. data/lib/releasehx/mcp/assets/config-def.yml +1426 -0
  34. data/lib/releasehx/mcp/assets/config-reference.adoc +4104 -0
  35. data/lib/releasehx/mcp/assets/config-reference.json +1546 -0
  36. data/lib/releasehx/mcp/assets/sample-config.yml +251 -0
  37. data/lib/releasehx/mcp/manifest.rb +18 -0
  38. data/lib/releasehx/mcp/resource_pack.rb +26 -0
  39. data/lib/releasehx/mcp/server.rb +57 -0
  40. data/lib/releasehx/mcp.rb +7 -0
  41. data/lib/releasehx/ops/check_ops.rb +136 -0
  42. data/lib/releasehx/ops/draft_ops.rb +173 -0
  43. data/lib/releasehx/ops/enrich_ops.rb +221 -0
  44. data/lib/releasehx/ops/template_ops.rb +61 -0
  45. data/lib/releasehx/ops/write_ops.rb +124 -0
  46. data/lib/releasehx/rest/clients/github.yml +46 -0
  47. data/lib/releasehx/rest/clients/gitlab.yml +31 -0
  48. data/lib/releasehx/rest/clients/jira.yml +31 -0
  49. data/lib/releasehx/rest/yaml_client.rb +418 -0
  50. data/lib/releasehx/rhyml/adapter.rb +740 -0
  51. data/lib/releasehx/rhyml/change.rb +167 -0
  52. data/lib/releasehx/rhyml/liquid.rb +13 -0
  53. data/lib/releasehx/rhyml/loaders.rb +37 -0
  54. data/lib/releasehx/rhyml/mappings/github.yaml +60 -0
  55. data/lib/releasehx/rhyml/mappings/gitlab.yaml +73 -0
  56. data/lib/releasehx/rhyml/mappings/jira.yaml +29 -0
  57. data/lib/releasehx/rhyml/mappings/verb_past_tenses.yml +98 -0
  58. data/lib/releasehx/rhyml/release.rb +144 -0
  59. data/lib/releasehx/rhyml.rb +15 -0
  60. data/lib/releasehx/sgyml/helpers.rb +45 -0
  61. data/lib/releasehx/transforms/adf_to_markdown.rb +307 -0
  62. data/lib/releasehx/version.rb +7 -0
  63. data/lib/releasehx.rb +69 -0
  64. data/lib/schemagraphy/attribute_resolver.rb +48 -0
  65. data/lib/schemagraphy/cfgyml/definition.rb +90 -0
  66. data/lib/schemagraphy/cfgyml/doc_builder.rb +52 -0
  67. data/lib/schemagraphy/cfgyml/path_reference.rb +24 -0
  68. data/lib/schemagraphy/data_query/json_pointer.rb +42 -0
  69. data/lib/schemagraphy/loader.rb +59 -0
  70. data/lib/schemagraphy/regexp_utils.rb +215 -0
  71. data/lib/schemagraphy/safe_expression.rb +189 -0
  72. data/lib/schemagraphy/schema_utils.rb +124 -0
  73. data/lib/schemagraphy/tag_utils.rb +32 -0
  74. data/lib/schemagraphy/templating.rb +104 -0
  75. data/lib/schemagraphy.rb +17 -0
  76. data/lib/sourcerer/builder.rb +120 -0
  77. data/lib/sourcerer/jekyll/bootstrapper.rb +78 -0
  78. data/lib/sourcerer/jekyll/liquid/file_system.rb +74 -0
  79. data/lib/sourcerer/jekyll/liquid/filters.rb +215 -0
  80. data/lib/sourcerer/jekyll/liquid/tags.rb +44 -0
  81. data/lib/sourcerer/jekyll/monkeypatches.rb +73 -0
  82. data/lib/sourcerer/jekyll.rb +26 -0
  83. data/lib/sourcerer/plaintext_converter.rb +75 -0
  84. data/lib/sourcerer/templating.rb +190 -0
  85. data/lib/sourcerer.rb +322 -0
  86. data/specs/data/api-client-schema.yaml +160 -0
  87. data/specs/data/config-def.yml +1426 -0
  88. data/specs/data/mcp-manifest.yml +50 -0
  89. data/specs/data/rhyml-mapping-schema.yaml +410 -0
  90. data/specs/data/rhyml-schema.yaml +152 -0
  91. metadata +376 -0
@@ -0,0 +1,173 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ReleaseHx
4
+ # The DraftOps module provides methods for creating Release objects and
5
+ # generating draft files from source data like API payloads.
6
+ module DraftOps
7
+ # Converts a raw JSON payload from an issue management system into a
8
+ # ReleaseHx::RHYML::Release object.
9
+ #
10
+ # @param payload [Hash, Array] The raw payload from the API.
11
+ # @param config [ReleaseHx::Configuration] The application configuration.
12
+ # @param mapping [Hash] The mapping definition for converting payload fields.
13
+ # @param release_code [String] The version code for the release.
14
+ # @param release_date [Date, String, nil] The date for the release.
15
+ # @param scan [Boolean] Indicates if this is a scan-only operation.
16
+ # @return [ReleaseHx::RHYML::Release] The generated Release object.
17
+ def self.from_payload payload:, config:, mapping:, release_code:, release_date: nil, scan: false
18
+ adapter = ReleaseHx::RHYML::Adapter.new(mapping: mapping, config: config)
19
+
20
+ adapter.to_release(
21
+ payload,
22
+ release_code: release_code,
23
+ release_date: release_date || Date.today,
24
+ scan: scan)
25
+ end
26
+
27
+ # Shared method for preparing RHYML object template context
28
+ def self.prepare_template_context release:, config:
29
+ raise ArgumentError, 'release is nil' if release.nil?
30
+
31
+ # Establish available datasets
32
+ all_changes = release.changes.select { |ch| ch.respond_to?(:to_h) }
33
+ changes_mapped = all_changes.map(&:to_h)
34
+
35
+ sorted = build_sorted_changes(changes_mapped, config)
36
+
37
+ context_scope = {
38
+ 'release' => release.to_h,
39
+ 'changes' => changes_mapped,
40
+ 'sorted' => sorted,
41
+ 'config' => config
42
+ }
43
+
44
+ SchemaGraphy::Templating.render_all_templated_fields!(config, context_scope)
45
+
46
+ {
47
+ variables: {
48
+ 'release' => release.to_h,
49
+ 'changes' => changes_mapped,
50
+ 'config' => config,
51
+ 'sorted' => sorted
52
+ },
53
+ context_scope: context_scope
54
+ }
55
+ end
56
+
57
+ # Generates a string of content for a draft file (e.g., Markdown, YAML)
58
+ # from a Release object using the configured Liquid templates.
59
+ #
60
+ # @param release [ReleaseHx::RHYML::Release] The release object.
61
+ # @param config [ReleaseHx::Configuration] The application configuration.
62
+ # @param format [Symbol] The output format (:yaml, :md, :adoc, :html).
63
+ # @return [String] The rendered content.
64
+ def self.process_template_content release:, config:, format:
65
+ context = prepare_template_context(release: release, config: config)
66
+
67
+ tplt = case format.to_sym
68
+ when :yaml, :yml then 'rhyml.yaml.liquid'
69
+ when :md then 'release.md.liquid'
70
+ when :adoc then 'release.adoc.liquid'
71
+ when :html then 'release.html.liquid'
72
+ else raise ArgumentError, "Unsupported format: #{format}"
73
+ end
74
+
75
+ tplt_path = WriteOps.resolve_template_path(tplt, config)
76
+ ReleaseHx.logger.debug "Using template: #{tplt_path}"
77
+
78
+ WriteOps.process_template(tplt_path, { 'vars' => context[:variables] }, config)
79
+ end
80
+
81
+ # Builds sorted changes structure (moved from EnrichOps)
82
+ def self.build_sorted_changes changes_mapped, config
83
+ sorted = {
84
+ 'by' => {
85
+ 'tag' => {},
86
+ 'type' => {},
87
+ 'part' => {}
88
+ }
89
+ }
90
+
91
+ changes_mapped.each do |ch|
92
+ Array(ch['tags']).each do |tag|
93
+ sorted['by']['tag'][tag] ||= []
94
+ sorted['by']['tag'][tag] << ch
95
+ end
96
+
97
+ type = ch['type']
98
+ if type
99
+ sorted['by']['type'][type] ||= []
100
+ sorted['by']['type'][type] << ch
101
+ end
102
+
103
+ # treat 'parts' differently from type or tags as it is an Array in all cases
104
+ parts = ch['parts']
105
+ next unless parts
106
+
107
+ Array(parts).each do |part|
108
+ sorted['by']['part'][part] ||= []
109
+ sorted['by']['part'][part] << ch
110
+ end
111
+ end
112
+
113
+ # Ensures all config-defined parts/types/tags are initialized
114
+ Array(config['types']&.keys).each do |type|
115
+ sorted['by']['type'][type] ||= []
116
+ end
117
+
118
+ Array(config['parts']&.keys).each do |part|
119
+ sorted['by']['part'][part] ||= []
120
+ end
121
+
122
+ Array(config['tags']&.keys).each do |tag|
123
+ next if %w[_include _exclude].include?(tag)
124
+
125
+ sorted['by']['tag'][tag] ||= []
126
+ end
127
+
128
+ sorted
129
+ end
130
+
131
+ # rubocop:disable Lint/UnusedMethodArgument
132
+ def self.append_changes yaml_file_path:, version_code:, config:, mapping:, source_type:, payload: nil, force: false
133
+ # NOTE: force parameter is currently unused but kept for API consistency
134
+ # Load existing YAML Release
135
+ existing_release = ReleaseHx::RHYML::ReleaseLoader.load(yaml_file_path)
136
+ existing_tick_ids = existing_release.changes.map(&:tick).compact
137
+
138
+ ReleaseHx.logger.debug "Found #{existing_tick_ids.size} existing changes with tick IDs: #{existing_tick_ids}"
139
+
140
+ # Use provided payload or default to API fetch
141
+ payload ||= fetch_payload_for_version(version_code, source_type, config)
142
+
143
+ new_release = from_payload(
144
+ payload: payload,
145
+ config: config,
146
+ mapping: mapping,
147
+ release_code: version_code,
148
+ release_date: existing_release.date)
149
+
150
+ # Find new changes that weren't in the existing YAML
151
+ new_changes = new_release.changes.select do |change|
152
+ change.tick && !existing_tick_ids.include?(change.tick)
153
+ end
154
+
155
+ return 0 if new_changes.empty?
156
+
157
+ ReleaseHx.logger.info "Found #{new_changes.size} new changes to append"
158
+
159
+ # Generate append content and write it
160
+ WriteOps.append_changes_to_yaml(yaml_file_path, new_changes, config)
161
+
162
+ new_changes.size
163
+ end
164
+ # rubocop:enable Lint/UnusedMethodArgument
165
+
166
+ # Legacy method kept for backward compatibility
167
+ def self.draft_output release:, config:, format:, outpath:
168
+ content = process_template_content(release: release, config: config, format: format)
169
+ WriteOps.safe_write(outpath, content) if outpath
170
+ content
171
+ end
172
+ end
173
+ end
@@ -0,0 +1,221 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ReleaseHx
4
+ # Provides rich-text output generation from Release objects and draft source files.
5
+ #
6
+ # The EnrichOps module handles the conversion of Release data and various draft formats
7
+ # (YAML, Markdown, AsciiDoc) into rich output formats (HTML, PDF) using appropriate
8
+ # rendering engines and template processing.
9
+ module EnrichOps
10
+ # Generates rich-text output directly from a Release object.
11
+ #
12
+ # Processes Release data through Liquid templates to produce HTML or PDF output.
13
+ # HTML generation uses direct template rendering, while PDF generation creates
14
+ # intermediate AsciiDoc content before conversion.
15
+ #
16
+ # @param release [ReleaseHx::RHYML::Release] The Release object to enrich.
17
+ # @param config [ReleaseHx::Configuration] The application configuration.
18
+ # @param format [Symbol] The output format (:html or :pdf).
19
+ # @param outpath [String, nil] The explicit output file path; auto-resolved if nil.
20
+ # @param force [Boolean] Whether to overwrite existing output files.
21
+ # @return [String] The path to the generated output file.
22
+ def self.enrich_from_rhyml release:, config:, format:, outpath: nil, force: false
23
+ # Use proper config-based output path resolution if not provided
24
+ outpath ||= resolve_enrich_path(release.code, format, config)
25
+
26
+ if File.exist?(outpath) && !force
27
+ ReleaseHx.logger.warn("File exists: #{outpath}. Use --force to overwrite.")
28
+ return outpath
29
+ end
30
+
31
+ ReleaseHx.logger.debug("Enriching release #{release.code} to #{outpath} (format: #{format.to_s.upcase})")
32
+
33
+ case format
34
+ when :html
35
+ # Direct Liquid template rendering to HTML
36
+ html_content = DraftOps.process_template_content(release: release, config: config, format: :html)
37
+ WriteOps.safe_write(outpath, html_content)
38
+ when :pdf
39
+ # Two-stage process: RHYML → AsciiDoc → PDF
40
+ asciidoc_content = DraftOps.process_template_content(release: release, config: config, format: :adoc)
41
+ convert_asciidoc(asciidoc_content, format: :pdf, outpath: outpath)
42
+ else
43
+ raise ArgumentError, "Unsupported enrich format: #{format}"
44
+ end
45
+
46
+ outpath
47
+ end
48
+
49
+ # Generates rich-text output from source draft files of various formats.
50
+ #
51
+ # Automatically detects the input file type and applies the appropriate conversion
52
+ # strategy, supporting YAML (via RHYML), Markdown, and AsciiDoc source formats.
53
+ #
54
+ # @param file_path [String] The path to the source draft file.
55
+ # @param format [Symbol] The target output format (:html or :pdf).
56
+ # @param config [ReleaseHx::Configuration] The application configuration.
57
+ # @param outpath [String, nil] The explicit output file path; inferred if nil.
58
+ # @param force [Boolean] Whether to overwrite existing output files.
59
+ # @return [String] The path to the generated output file.
60
+ def self.enrich_from_file file_path, format:, config:, outpath: nil, force: false
61
+ raise "File not found: #{file_path}" unless File.exist?(file_path)
62
+
63
+ file_ext = File.extname(file_path).downcase
64
+ outpath ||= file_path.sub(/\.[^.]+$/, ".#{format}")
65
+
66
+ # Prevent accidental file overwrites
67
+ if File.exist?(outpath) && !force
68
+ ReleaseHx.logger.warn("File exists: #{outpath}. Use --force to overwrite.")
69
+ return outpath
70
+ end
71
+
72
+ # Route to appropriate conversion method based on source format
73
+ case file_ext
74
+ when '.yml', '.yaml'
75
+ # RHYML/YAML files: load as Release object and use Liquid templates
76
+ release = load_rhyml_from_yaml(file_path, config: config)
77
+ enrich_from_rhyml(release: release, config: config, format: format, outpath: outpath, force: true)
78
+ when '.md'
79
+ # Markdown files: use native converter
80
+ convert_markdown(file_path, format: format, config: config, outpath: outpath, force: true)
81
+ when '.adoc'
82
+ # AsciiDoc files: use native converter
83
+ convert_asciidoc(file_path, format: format, config: config, outpath: outpath, force: true)
84
+ else
85
+ raise "Unsupported source file format: #{file_ext}"
86
+ end
87
+ end
88
+
89
+ # Converts Markdown files to rich-text formats using native converters.
90
+ #
91
+ # Uses Kramdown for HTML generation and Tilt with Pandoc for PDF conversion.
92
+ # Note: Some parameters are reserved for API consistency across conversion methods.
93
+ #
94
+ # @param file_path [String] The path to the source Markdown file.
95
+ # @param format [Symbol] The target output format (:html or :pdf).
96
+ # @param config [ReleaseHx::Configuration] Reserved for future use.
97
+ # @param outpath [String] The target output file path.
98
+ # @param force [Boolean] Reserved for future use.
99
+ # @return [String] The path to the generated output file.
100
+ # rubocop:disable Lint/UnusedMethodArgument
101
+ def self.convert_markdown file_path, format:, config:, outpath:, force:
102
+ # NOTE: config and force parameters are currently unused but kept for API consistency
103
+ content = File.read(file_path)
104
+
105
+ case format.to_sym
106
+ when :html
107
+ require 'kramdown'
108
+ enriched = Kramdown::Document.new(content).to_html
109
+ WriteOps.safe_write(outpath, enriched)
110
+ when :pdf
111
+ # Use Tilt with Pandoc for direct Markdown to PDF conversion
112
+ require 'tilt'
113
+ require 'tilt/pandoc'
114
+
115
+ template = Tilt::PandocTemplate.new(file_path)
116
+ enriched = template.render(nil, to: 'pdf')
117
+ WriteOps.safe_write(outpath, enriched)
118
+ ReleaseHx.logger.info("PDF generated via Tilt/Pandoc: #{outpath}")
119
+ else
120
+ raise "Unsupported format for Markdown: #{format}"
121
+ end
122
+
123
+ outpath
124
+ end
125
+ # rubocop:enable Lint/UnusedMethodArgument
126
+
127
+ # Converts AsciiDoc content or files to rich-text formats using Asciidoctor.
128
+ #
129
+ # Accepts either file paths or raw content strings as input. Uses Asciidoctor
130
+ # for HTML generation and Asciidoctor-PDF for direct PDF output.
131
+ #
132
+ # @param file_path_or_content [String] File path or raw AsciiDoc content.
133
+ # @param format [Symbol] The target output format (:html or :pdf).
134
+ # @param outpath [String] The target output file path.
135
+ # @param config [ReleaseHx::Configuration] Reserved for future use.
136
+ # @param force [Boolean] Reserved for future use.
137
+ # @return [String] The path to the generated output file.
138
+ # rubocop:disable Lint/UnusedMethodArgument
139
+ def self.convert_asciidoc file_path_or_content, format:, outpath:, config: nil, force: nil
140
+ # NOTE: config and force parameters are currently unused but kept for API consistency
141
+ # Determine if input is file path or raw content
142
+ is_file = file_path_or_content.is_a?(String) && File.exist?(file_path_or_content)
143
+ content = is_file ? File.read(file_path_or_content) : file_path_or_content
144
+
145
+ case format.to_sym
146
+ when :html
147
+ require 'asciidoctor'
148
+ enriched = Asciidoctor.convert(content, safe: :safe, backend: 'html5')
149
+ when :pdf
150
+ require 'asciidoctor'
151
+ require 'asciidoctor-pdf'
152
+ Asciidoctor.convert(content, to_file: outpath, safe: :safe, backend: 'pdf')
153
+ return outpath
154
+ else
155
+ raise "Unsupported format for AsciiDoc: #{format}"
156
+ end
157
+
158
+ # Prevent accidental file overwrites for HTML output
159
+ if File.exist?(outpath) && !force
160
+ ReleaseHx.logger.warn("File exists: #{outpath}. Use --force to overwrite.")
161
+ return outpath
162
+ end
163
+ WriteOps.safe_write(outpath, enriched)
164
+ outpath
165
+ end
166
+ # rubocop:enable Lint/UnusedMethodArgument
167
+
168
+ # Loads RHYML data from a YAML file and creates a Release object.
169
+ #
170
+ # Processes YAML files containing either a single Release or a collection
171
+ # of Releases, extracting the first Release for processing.
172
+ #
173
+ # @param file_path [String] Path to the YAML file containing RHYML data.
174
+ # @param config [ReleaseHx::Configuration] Reserved for future use.
175
+ # @return [ReleaseHx::RHYML::Release] The constructed Release object.
176
+ # rubocop:disable Lint/UnusedMethodArgument
177
+ def self.load_rhyml_from_yaml file_path, config:
178
+ # NOTE: config parameter is currently unused but kept for API consistency
179
+ # Load RHYML data using SchemaGraphy for tag processing
180
+ rhyml_data = SchemaGraphy::Loader.load_yaml_with_tags(file_path)
181
+
182
+ # Extract Release data: first item from 'releases' array or single Release hash
183
+ release_data = rhyml_data['releases'] ? rhyml_data['releases'].first : rhyml_data
184
+
185
+ # Construct Release object with keyword arguments
186
+ ReleaseHx::RHYML::Release.new(
187
+ code: release_data['code'],
188
+ date: release_data['date'],
189
+ hash: release_data['hash'],
190
+ memo: release_data['memo'],
191
+ changes: release_data['changes'] || [])
192
+ end
193
+ # rubocop:enable Lint/UnusedMethodArgument
194
+
195
+ # Resolves the output file path using configuration-based templated filenames.
196
+ #
197
+ # Constructs the full output path by combining configured directories and
198
+ # processing template variables in the filename pattern.
199
+ #
200
+ # @param version [String] The release version code for template substitution.
201
+ # @param format [Symbol] The output format for file extension determination.
202
+ # @param config [ReleaseHx::Configuration] Configuration containing path templates.
203
+ # @return [String] The resolved absolute output file path.
204
+ def self.resolve_enrich_path version, format, config
205
+ # Extract path configuration from config
206
+ output_dir = config.dig('paths', 'output_dir')
207
+ enrich_dir = config.dig('paths', 'enrich_dir')
208
+ filename_template = config.dig('paths', 'enrich_filename')
209
+
210
+ # Build template context for filename processing
211
+ context = {
212
+ 'version' => version,
213
+ 'format_ext' => format.to_s
214
+ }
215
+
216
+ # Process templated filename and construct full path
217
+ filename = SchemaGraphy::Templating.render_field_if_template(filename_template, context)
218
+ File.join(output_dir, enrich_dir, filename.strip)
219
+ end
220
+ end
221
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pathname'
4
+
5
+ module ReleaseHx
6
+ # Provides Liquid template rendering services with extended Jekyll integration and configurable template paths.
7
+ #
8
+ # The TemplateOps module handles the complex setup required for Liquid template processing, including
9
+ # Jekyll plugin loading, template path resolution, and Sourcerer Jekyll runtime initialization for
10
+ # advanced template features and includes.
11
+ module TemplateOps
12
+ # Renders a Liquid template string with provided variables and configuration context.
13
+ #
14
+ # Sets up a complete, enhanced Liquid rendering environment with plugin support,
15
+ # configurable template include paths, and proper site context for advanced template features.
16
+ # Supports both user-defined and gem-bundled template directories with fallback resolution.
17
+ #
18
+ # @param input [String] The raw Liquid template string to process.
19
+ # @param vars [Hash] Variable context for template variable substitution.
20
+ # @param config [ReleaseHx::Configuration] Configuration object containing template path settings.
21
+ # @return [String] The fully rendered template output.
22
+ # @raise [StandardError] If template rendering fails due to syntax errors or missing variables.
23
+ def self.render_liquid_string input, vars, config
24
+ plugin_dirs = [File.expand_path('../../../jekyll_plugins', __dir__)]
25
+ user_templates_dir = config.dig('paths', 'templates_dir')
26
+ gem_templates_dir = File.expand_path('../rhyml/templates', __dir__)
27
+
28
+ # Build template include path hierarchy: user templates take precedence over gem defaults
29
+ includes = []
30
+ if user_templates_dir
31
+ unless Pathname.new(user_templates_dir).absolute?
32
+ user_templates_dir = File.expand_path(user_templates_dir, Dir.pwd)
33
+ end
34
+ includes << user_templates_dir if File.directory?(user_templates_dir)
35
+ end
36
+ includes << gem_templates_dir
37
+
38
+ Sourcerer::Jekyll.initialize_liquid_runtime
39
+
40
+ # Bootstrap an ephemeral Jekyll site context for template processing with full feature support
41
+ site = Sourcerer::Jekyll::Bootstrapper.fake_site(
42
+ includes_load_paths: includes,
43
+ plugin_dirs: plugin_dirs)
44
+
45
+ # Parse template and render with comprehensive register context
46
+ tpl = ::Liquid::Template.parse(input)
47
+ rendered = tpl.render(
48
+ vars,
49
+ registers: {
50
+ site: site,
51
+ file_system: Sourcerer::Jekyll::Liquid::FileSystem.new(includes.first),
52
+ includes_load_paths: includes,
53
+ releasehx_debug: true
54
+ })
55
+
56
+ raise "Template rendering failed:\n#{rendered}" if rendered.include?('Liquid error')
57
+
58
+ rendered
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'tilt'
5
+ require_relative 'template_ops'
6
+
7
+ module ReleaseHx
8
+ # Provides file writing operations and template processing utilities for output generation.
9
+ #
10
+ # The WriteOps module handles safe file creation with directory management, template resolution
11
+ # with user/gem fallback paths, and content post-processing for consistent output formatting.
12
+ module WriteOps
13
+ # Safely writes content to a file, creating parent directories as needed.
14
+ #
15
+ # Ensures parent directory structure exists before writing and provides
16
+ # logging feedback for successful file creation operations.
17
+ #
18
+ # @param path [String] The target file path for writing.
19
+ # @param content [String] The content to write to the file.
20
+ # @return [Symbol] Returns :written to indicate successful completion.
21
+ def self.safe_write path, content
22
+ dirname = File.dirname(path)
23
+ FileUtils.mkdir_p(dirname)
24
+ File.write(path, content)
25
+ ReleaseHx.logger.info "Wrote file: #{path}"
26
+ :written
27
+ end
28
+
29
+ # Establishes the absolute path to the gem's bundled template directory.
30
+ #
31
+ # @return [String] The path to the default gem template directory.
32
+ def self.gem_template_root
33
+ File.expand_path('../rhyml/templates', __dir__)
34
+ end
35
+
36
+ # Resolves template file path using hierarchical search with user directory precedence.
37
+ #
38
+ # Searches for templates first in user-configured directories, then falls back to
39
+ # gem-bundled templates, providing clear error messages when templates are not found.
40
+ #
41
+ # @param name [String] The template filename to locate.
42
+ # @param config [ReleaseHx::Configuration] Configuration object containing template path settings.
43
+ # @return [String] The absolute path to the located template file.
44
+ # @raise [StandardError] If the template cannot be found in any search path.
45
+ def self.resolve_template_path name, config
46
+ user_dir = config.dig('paths', 'templates_dir')
47
+ fallback_dir = gem_template_root
48
+
49
+ search_paths = []
50
+ search_paths << File.expand_path(name, user_dir) if user_dir
51
+ search_paths << File.join(fallback_dir, name)
52
+
53
+ found = search_paths.find { |p| File.exist?(p) }
54
+
55
+ return found if found
56
+
57
+ raise "Template not found: #{name} (searched #{search_paths.join(' , ')})"
58
+ end
59
+
60
+ # Processes a Liquid template file with variable substitution and content post-processing.
61
+ #
62
+ # Loads and renders templates using the TemplateOps system, then applies configurable
63
+ # post-processing rules such as excess line removal for consistent output formatting.
64
+ #
65
+ # @param template_path [String] The absolute path to the template file.
66
+ # @param vars [Hash] Variable context for template rendering.
67
+ # @param config [ReleaseHx::Configuration] Configuration object with processing settings.
68
+ # @return [String] The fully processed template output.
69
+ def self.process_template template_path, vars, config
70
+ template_content = File.read(template_path)
71
+ includes_load_paths = []
72
+ user_templates_dir = config.dig('paths', 'templates_dir')
73
+ gem_templates_dir = File.expand_path('../../rhyml/templates', __dir__)
74
+
75
+ if user_templates_dir
76
+ unless Pathname.new(user_templates_dir).absolute?
77
+ user_templates_dir = File.expand_path(user_templates_dir, Dir.pwd)
78
+ end
79
+ includes_load_paths << user_templates_dir
80
+ end
81
+
82
+ includes_load_paths << gem_templates_dir
83
+
84
+ rendered = TemplateOps.render_liquid_string(template_content, vars, config)
85
+
86
+ # Apply configurable post-processing for line spacing control
87
+ blank_lines = config.dig('modes', 'remove_excess_lines')
88
+ if blank_lines.zero?
89
+ rendered = rendered.gsub(/\n{2,}/, "\n")
90
+ elsif blank_lines
91
+ rendered = rendered.gsub(/\n{#{blank_lines + 1},}/, "\n" * (blank_lines + 1))
92
+ end
93
+
94
+ rendered
95
+ end
96
+
97
+ # Appends new changes to an existing YAML file using template-based formatting.
98
+ #
99
+ # Generates properly formatted YAML content for new changes using a template,
100
+ # then appends it to the specified file while maintaining file structure.
101
+ #
102
+ # @param yaml_file_path [String] The path to the target YAML file for appending.
103
+ # @param new_changes [Array<ReleaseHx::RHYML::Change>] Array of Change objects to append.
104
+ # @param config [ReleaseHx::Configuration] Configuration object for template processing.
105
+ # @return [void]
106
+ def self.append_changes_to_yaml yaml_file_path, new_changes, config
107
+ # Generate properly formatted YAML content using template
108
+ context = {
109
+ 'changes' => new_changes.map(&:to_h),
110
+ 'config' => config
111
+ }
112
+
113
+ template_path = resolve_template_path('rhyml-change-append.yaml.liquid', config)
114
+ append_content = process_template(template_path, { 'vars' => context }, config)
115
+
116
+ # Append to existing file maintaining structure
117
+ File.open(yaml_file_path, 'a') do |file|
118
+ file.write(append_content)
119
+ end
120
+
121
+ ReleaseHx.logger.debug "Appended #{new_changes.size} changes to #{yaml_file_path}"
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,46 @@
1
+ # GitHub REST API Client Configuration
2
+ # For GitHub v3 API - Issues endpoint
3
+ name: github
4
+ desc: |
5
+ GitHub REST API client for fetching issues and pull requests.
6
+ Supports GitHub v3 API with token-based authentication.
7
+ Includes dynamic milestone resolution.
8
+
9
+ auth:
10
+ mode: header
11
+ header: Authorization
12
+ format: "Bearer {{ env[auth.key_env] | default: env.GITHUB_TOKEN }}"
13
+
14
+ # GitHub API endpoint template
15
+ # Variables available: project, version, env (flattened from api config)
16
+ href: "{{ env.GITHUB_API_HOST | default: 'https://api.github.com' }}/repos/{{ project | default: env.GITHUB_REPO }}/issues"
17
+ query_type: key_value
18
+
19
+ # Dynamic resolution for milestone titles -> numbers
20
+ resolutions:
21
+ milestone:
22
+ endpoint: "/repos/{{ project | default: env.GITHUB_REPO }}/milestones"
23
+ query_param: "milestone" # which param in main query needs resolution
24
+ lookup_field: "title" # field to match against (milestone title)
25
+ return_field: "number" # field to use as resolved value
26
+ match_value: "{{ version }}" # what to match (template)
27
+
28
+ # Structured query parameters - much cleaner than query_string!
29
+ # The {{ milestone }} template will be resolved to the milestone number
30
+ query_params:
31
+ milestone: "{{ milestone }}" # will be resolved to milestone number
32
+ state: "closed"
33
+ sort: "updated"
34
+ direction: "desc"
35
+ per_page: 100
36
+
37
+ # Pagination settings using GitHub's Link header
38
+ pagination:
39
+ param: page
40
+ page_size_param: per_page
41
+ page_size: 100
42
+ max_pages: 10
43
+
44
+ # Response format expectation
45
+ response_format: json
46
+ root_issues_path: "."
@@ -0,0 +1,31 @@
1
+ # GitLab REST API Client Configuration
2
+ # For GitLab v4 API - Issues endpoint
3
+ name: gitlab
4
+ desc: |
5
+ GitLab REST API client for fetching issues and merge requests.
6
+ Supports GitLab v4 API with token-based authentication.
7
+
8
+ auth:
9
+ mode: header
10
+ header: PRIVATE-TOKEN
11
+ format: "{{ env.GITLAB_TOKEN }}"
12
+
13
+ # GitLab API endpoint template
14
+ # Variables available: origin.project, version, env
15
+ href: "{{ env.GITLAB_API_HOST | default: 'https://gitlab.com' }}/api/v4/projects/{{ origin.project | default: env.GITLAB_PROJECT_ID }}/issues"
16
+ query_type: key_value
17
+
18
+ # Query parameters for filtering issues
19
+ query_string: |
20
+ milestone={{ version }}&state=closed&per_page=100
21
+
22
+ # Pagination settings using GitLab's offset pagination
23
+ pagination:
24
+ param: page
25
+ page_size_param: per_page
26
+ page_size: 100
27
+ max_pages: 10
28
+
29
+ # Response format expectation
30
+ response_format: json
31
+ root_issues_path: "."
@@ -0,0 +1,31 @@
1
+ # Jira REST API Client Configuration
2
+ # Jira Cloud API with ADF (Atlassian Document Format) support
3
+ name: jira
4
+ desc: |
5
+ Jira Cloud REST API client for fetching issues and creating release notes.
6
+ Supports ADF format in description and custom fields.
7
+
8
+ auth:
9
+ mode: basic
10
+ header: Authorization
11
+ format: "Basic {{ env.JIRA_USERNAME | append: ':' | append: env.JIRA_API_TOKEN | base64 }}"
12
+
13
+ # Jira REST API endpoint template
14
+ # Variables available: origin.project, version, env
15
+ href: "{{ env.JIRA_HOST | default: 'https://your-domain.atlassian.net' }}/rest/api/3/search/jql"
16
+
17
+ # Query parameters for filtering issues
18
+ query_params:
19
+ jql: 'project="{{ origin.project | default: env.JIRA_PROJECT }}" AND fixVersion="{{ version }}" ORDER BY updated DESC'
20
+ fields: "key,summary,description,issuetype,components,labels,fixVersions,assignee"
21
+
22
+ # Pagination settings for large result sets
23
+ pagination:
24
+ param: startAt
25
+ page_size_param: maxResults
26
+ page_size: 50
27
+ max_pages: 20
28
+
29
+ # Response format expectation
30
+ response_format: json
31
+ root_issues_path: issues