Skip to content

Commit ecc8580

Browse files
GvieveGenevieve Nuebel
andauthored
Workflow Cleanup & Multi-Version Improvements (#72)
* Create ChangelogManager, use in workflows, update docs * Add gemfile and workflow to run specs * Update workflows to address errors/duplicate logic * Update on push to use serial job and gate * Update all docs based on workflow changes * Cleanup tests and changelog code * Create validator for config file and api -> major version * Update config validator and includes in all docs * Rename generate_publish_release and all doc references * Remove ruby 3.0 * Re-audit of docs and so many updates * Add Changed subtitle to new entries * Fix on push master...hopefully * Please oh please let this one work * Fix more no nos I found * Remove last references for config --------- Co-authored-by: Genevieve Nuebel <genevieve.nuebel@mx.com>
1 parent 0cc19c6 commit ecc8580

26 files changed

+1641
-432
lines changed

.github/changelog_manager.rb

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
require 'json'
2+
require 'date'
3+
4+
class ChangelogManager
5+
CHANGELOG_PATH = 'CHANGELOG.md'.freeze
6+
BASE_PATH = '.'.freeze
7+
TODAY = Date.today.freeze
8+
9+
# API versions in priority order (newest first)
10+
# This ensures most recent API version entries appear before older API versions in the log
11+
API_VERSION_ORDER = ['v20250224', 'v20111101'].freeze
12+
13+
class << self
14+
def run(versions_arg)
15+
validate_versions(versions_arg)
16+
update(versions_arg)
17+
puts "✅ CHANGELOG updated successfully"
18+
end
19+
20+
def update(versions)
21+
versions_array = normalize_versions(versions)
22+
23+
unless File.exist?(CHANGELOG_PATH)
24+
raise "Changelog not found at #{CHANGELOG_PATH}"
25+
end
26+
27+
# Read version numbers from each version's package.json
28+
version_data = versions_array.map do |api_version|
29+
version_number = read_package_version(api_version)
30+
raise "Could not read version from #{api_version}/package.json" if version_number.nil? || version_number.empty?
31+
[api_version, version_number]
32+
end
33+
34+
sorted_data = sort_versions(version_data)
35+
current_changelog = File.read(CHANGELOG_PATH)
36+
37+
# Build changelog entries for each version and updated changelog
38+
entries = sorted_data.map { |api_version, version_num| build_entry(api_version, version_num, TODAY) }
39+
updated_changelog = insert_entries(current_changelog, entries)
40+
41+
# Write back to file
42+
File.write(CHANGELOG_PATH, updated_changelog)
43+
44+
true
45+
end
46+
47+
private
48+
49+
def validate_versions(versions_arg)
50+
if versions_arg.nil? || versions_arg.empty?
51+
puts "Usage: ruby changelog_manager.rb <versions>"
52+
puts "Example: ruby changelog_manager.rb 'v20250224,v20111101'"
53+
puts "Supported versions: #{API_VERSION_ORDER.join(', ')}"
54+
exit 1
55+
end
56+
57+
if has_invalid_versions?(versions_arg)
58+
puts "❌ Error: Invalid versions. Supported versions: #{API_VERSION_ORDER.join(', ')}"
59+
exit 1
60+
end
61+
end
62+
63+
def has_invalid_versions?(versions_arg)
64+
versions_array = versions_arg.split(',').map(&:strip)
65+
invalid_versions = versions_array - API_VERSION_ORDER
66+
invalid_versions.any?
67+
end
68+
69+
def normalize_versions(versions)
70+
case versions
71+
when String
72+
versions.split(',').map(&:strip)
73+
when Array
74+
versions.map(&:to_s)
75+
else
76+
raise "Versions must be String or Array, got #{versions.class}"
77+
end
78+
end
79+
80+
def read_package_version(api_version)
81+
package_json_path = File.join(BASE_PATH, api_version, 'package.json')
82+
83+
unless File.exist?(package_json_path)
84+
raise "Package file not found at #{package_json_path}"
85+
end
86+
87+
package_json = JSON.parse(File.read(package_json_path))
88+
package_json['version']
89+
end
90+
91+
92+
def sort_versions(version_data)
93+
version_data.sort_by do |api_version, _|
94+
order_index = API_VERSION_ORDER.index(api_version)
95+
order_index || Float::INFINITY
96+
end
97+
end
98+
99+
def build_entry(api_version, version_number, date)
100+
date_str = date.strftime('%Y-%m-%d')
101+
last_change_date = extract_last_change_date(api_version)
102+
103+
if last_change_date
104+
last_change_str = last_change_date.strftime('%Y-%m-%d')
105+
message = "Updated #{api_version} API specification to most current version. Please check full [API changelog](https://docs.mx.com/resources/changelog/platform) for any changes made between #{last_change_str} and #{date_str}."
106+
else
107+
message = "Updated #{api_version} API specification to most current version. Please check full [API changelog](https://docs.mx.com/resources/changelog/platform) for any changes."
108+
end
109+
110+
<<~ENTRY
111+
## [#{version_number}] - #{date_str} (#{api_version} API)
112+
113+
### Changed
114+
- #{message}
115+
ENTRY
116+
end
117+
118+
# Extract the date of the last change for a given API version from the changelog
119+
# Finds the first entry in the changelog that mentions the api_version
120+
# such as "v20250224" and returns date of last change or nil if not found
121+
def extract_last_change_date(api_version)
122+
return nil unless File.exist?(CHANGELOG_PATH)
123+
124+
File.readlines(CHANGELOG_PATH).each do |line|
125+
# Look for lines like: ## [2.0.0] - 2025-01-15 (v20111101 API)
126+
if line.match?(/## \[\d+\.\d+\.\d+\]\s*-\s*(\d{4}-\d{2}-\d{2})\s*\(#{Regexp.escape(api_version)}\s+API\)/)
127+
# Extract the date from the line
128+
match = line.match(/(\d{4}-\d{2}-\d{2})/)
129+
return Date.parse(match[1]) if match
130+
end
131+
end
132+
133+
nil
134+
end
135+
136+
# Insert entries into changelog after the header section
137+
# Finds the first ## entry and inserts new entries before it
138+
def insert_entries(changelog, entries)
139+
lines = changelog.split("\n")
140+
141+
first_entry_index = lines.find_index { |line| line.start_with?('## [') }
142+
143+
if first_entry_index.nil?
144+
raise "Could not find existing changelog entries. Expected format: ## [version]"
145+
end
146+
147+
header = lines[0...first_entry_index]
148+
rest = lines[first_entry_index..]
149+
150+
# Combine: header + new entries + rest
151+
(header + entries.map { |e| e.rstrip } + [''] + rest).join("\n")
152+
end
153+
end
154+
end
155+
156+
# CLI Interface - allows usage from GitHub Actions
157+
ChangelogManager.run(ARGV[0]) if __FILE__ == $0

.github/config_validator.rb

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
require "yaml"
2+
3+
# ConfigValidator validates SDK configuration files before generation
4+
# Ensures semantic versioning rules are enforced and configs are properly structured
5+
class ConfigValidator
6+
SUPPORTED_VERSIONS = {
7+
"v20111101" => 2,
8+
"v20250224" => 3
9+
}.freeze
10+
11+
def self.validate!(config_file, api_version)
12+
new(config_file, api_version).validate!
13+
end
14+
15+
def initialize(config_file, api_version)
16+
@config_file = config_file
17+
@api_version = api_version
18+
end
19+
20+
def validate!
21+
check_api_version_supported!
22+
check_config_exists!
23+
check_config_readable!
24+
validate_semantic_versioning!
25+
true
26+
end
27+
28+
private
29+
30+
def check_api_version_supported!
31+
unless SUPPORTED_VERSIONS.key?(@api_version)
32+
supported = SUPPORTED_VERSIONS.keys.join(", ")
33+
raise "Invalid API version: #{@api_version}. Supported versions: #{supported}"
34+
end
35+
end
36+
37+
def check_config_exists!
38+
unless File.exist?(@config_file)
39+
raise "Config file not found: #{@config_file}"
40+
end
41+
end
42+
43+
def check_config_readable!
44+
begin
45+
config = YAML.load(File.read(@config_file))
46+
# YAML.load can return a string if file contains only invalid YAML
47+
# We need to ensure it returned a Hash (parsed YAML object)
48+
unless config.is_a?(Hash)
49+
raise "Config file does not contain valid YAML structure: #{@config_file}"
50+
end
51+
rescue Psych::SyntaxError => e
52+
raise "Config file syntax error in #{@config_file}: #{e.message}"
53+
rescue StandardError => e
54+
raise "Could not read config file #{@config_file}: #{e.message}"
55+
end
56+
end
57+
58+
def validate_semantic_versioning!
59+
config = YAML.load(File.read(@config_file))
60+
61+
unless config.key?("npmVersion")
62+
raise "Config missing npmVersion field: #{@config_file}"
63+
end
64+
65+
npm_version = config["npmVersion"].to_s.strip
66+
major_version = npm_version.split(".")[0].to_i
67+
68+
expected_major = SUPPORTED_VERSIONS[@api_version]
69+
70+
if major_version != expected_major
71+
raise "Semantic versioning error: #{@api_version} API must use npm major version #{expected_major}, " \
72+
"found #{major_version} in #{@config_file}\n" \
73+
"Current npmVersion: #{npm_version}\n" \
74+
"Update config with correct major version: #{expected_major}.x.x"
75+
end
76+
end
77+
end
78+
79+
# CLI Interface - allows direct execution from GitHub Actions
80+
ConfigValidator.validate!(ARGV[0], ARGV[1]) if __FILE__ == $0

0 commit comments

Comments
 (0)