Skip to content

Commit e7446b8

Browse files
authored
Merge pull request #1138 from owasp-noir/plucky-bobcat
Fix version check for updated docs and add version-update script
2 parents 060f15a + ae7a54a commit e7446b8

3 files changed

Lines changed: 194 additions & 25 deletions

File tree

justfile

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ alias b := build
22
alias ds := docs-serve
33
alias dsup := docs-supported
44
alias vc := version-check
5+
alias vu := version-update
56

67
# List available tasks.
78
default:
@@ -78,3 +79,8 @@ test-uncovered:
7879
[group('development')]
7980
version-check:
8081
crystal run scripts/check_version_consistency.cr
82+
83+
# Update version across all files (uses shard.yml version, or specify new version).
84+
[group('development')]
85+
version-update VERSION="":
86+
@if [ -z "{{VERSION}}" ]; then crystal run scripts/version_update.cr; else crystal run scripts/version_update.cr -- {{VERSION}}; fi

scripts/check_version_consistency.cr

Lines changed: 5 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -128,30 +128,8 @@ class VersionChecker
128128
end
129129

130130
private def check_docs_index_file(file_path : String) : CheckResult
131-
# Check both version and badge fields in documentation index files
132-
expected = "v#{@shard_version}"
133-
134-
if File.exists?(file_path)
135-
content = File.read(file_path)
136-
version_match = content.match(/version\s*=\s*"v([^"]+)"/)
137-
badge_match = content.match(/badge\s*=\s*"v([^"]+)"/)
138-
139-
if version_match && badge_match
140-
version_value = "v#{version_match[1]}"
141-
badge_value = "v#{badge_match[1]}"
142-
143-
if version_value == expected && badge_value == expected
144-
CheckResult.new(file_path, "version and badge", expected, expected, true)
145-
else
146-
actual = "version=#{version_value}, badge=#{badge_value}"
147-
CheckResult.new(file_path, "version and badge", expected, actual, false)
148-
end
149-
else
150-
CheckResult.new(file_path, "version and badge", expected, nil, false)
151-
end
152-
else
153-
CheckResult.new(file_path, "version and badge", expected, nil, false)
154-
end
131+
# Check hero-badge version in documentation index files
132+
check_file(file_path, /class="hero-badge">v([\d.]+)</, @shard_version)
155133
end
156134

157135
private def check_docs_index_md : CheckResult
@@ -177,7 +155,9 @@ class VersionChecker
177155
end
178156

179157
private def check_copilot_instructions : CheckResult
180-
check_file("AGENTS.md", /shard\.yml.*version:\s*([^\)]+)\)/, @shard_version)
158+
# AGENTS.md no longer contains a hardcoded version string.
159+
# Version consistency is maintained through shard.yml only.
160+
CheckResult.new("AGENTS.md", "N/A (no version)", @shard_version, @shard_version, true)
181161
end
182162

183163
private def check_how_to_release_md : CheckResult

scripts/version_update.cr

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
#!/usr/bin/env crystal
2+
# noir/scripts/version_update.cr
3+
# Update version across all files using shard.yml as source of truth
4+
#
5+
# Usage:
6+
# crystal run scripts/version_update.cr # Update all files to shard.yml version
7+
# crystal run scripts/version_update.cr -- 0.29.0 # Set new version in shard.yml and all files
8+
# just version-update # Update all files to shard.yml version
9+
# just version-update 0.29.0 # Set new version everywhere
10+
#
11+
# Exit codes:
12+
# 0 - All files updated successfully
13+
# 1 - Error during update
14+
# 2 - Error reading files or invalid format
15+
16+
require "yaml"
17+
18+
class VersionUpdater
19+
# Each replacement target: file path, regex pattern, replacement template.
20+
# The template uses `%{version}` for bare version and `%{v_version}` for "v"-prefixed.
21+
record Target,
22+
file_path : String,
23+
pattern : Regex,
24+
replacement : String
25+
26+
TARGETS = [
27+
Target.new(
28+
"shard.yml",
29+
/^(version:\s*)[\d.]+/m,
30+
"\\1%{version}"
31+
),
32+
Target.new(
33+
"src/noir.cr",
34+
/(VERSION\s*=\s*")[\d.]+(")/,
35+
"\\1%{version}\\2"
36+
),
37+
Target.new(
38+
"flake.nix",
39+
/(version\s*=\s*")[\d.]+(")/,
40+
"\\1%{version}\\2"
41+
),
42+
Target.new(
43+
"Dockerfile",
44+
/(org\.opencontainers\.image\.version=")[\d.]+(")/,
45+
"\\1%{version}\\2"
46+
),
47+
Target.new(
48+
"snap/snapcraft.yaml",
49+
/^(version:\s*)[\d.]+/m,
50+
"\\1%{version}"
51+
),
52+
Target.new(
53+
"docs/content/_index.md",
54+
/(class="hero-badge">)v[\d.]+(<\/)/,
55+
"\\1%{v_version}\\2"
56+
),
57+
Target.new(
58+
"docs/content/_index.ko.md",
59+
/(class="hero-badge">)v[\d.]+(<\/)/,
60+
"\\1%{v_version}\\2"
61+
),
62+
Target.new(
63+
"github-action/Dockerfile",
64+
/(FROM\s+ghcr\.io\/owasp-noir\/noir:)v[\d.]+/,
65+
"\\1%{v_version}"
66+
),
67+
Target.new(
68+
"github-action/README.md",
69+
/(uses:\s+owasp-noir\/noir@)v[\d.]+/,
70+
"\\1%{v_version}"
71+
),
72+
Target.new(
73+
"docs/content/development/how_to_release/index.md",
74+
/(brew bump-formula-pr --strict --version\s+)[\d.]+(\s+noir)/,
75+
"\\1%{version}\\2"
76+
),
77+
Target.new(
78+
"docs/content/development/how_to_release/index.ko.md",
79+
/(brew bump-formula-pr --strict --version\s+)[\d.]+(\s+noir)/,
80+
"\\1%{version}\\2"
81+
),
82+
]
83+
84+
getter version : String
85+
86+
def initialize(@version : String)
87+
end
88+
89+
def run : Int32
90+
puts "Updating version to #{@version} across all files...\n"
91+
92+
errors = [] of String
93+
94+
TARGETS.each do |target|
95+
result = update_file(target)
96+
if result
97+
puts "#{target.file_path}"
98+
else
99+
puts "#{target.file_path}"
100+
errors << target.file_path
101+
end
102+
end
103+
104+
puts
105+
if errors.empty?
106+
puts "🎉 All files updated to #{@version}!"
107+
0
108+
else
109+
puts "❌ Failed to update #{errors.size} file(s):"
110+
errors.each { |f| puts " - #{f}" }
111+
1
112+
end
113+
end
114+
115+
private def update_file(target : Target) : Bool
116+
unless File.exists?(target.file_path)
117+
STDERR.puts " Warning: #{target.file_path} not found, skipping"
118+
return false
119+
end
120+
121+
content = File.read(target.file_path)
122+
unless content.match(target.pattern)
123+
STDERR.puts " Warning: pattern not found in #{target.file_path}"
124+
return false
125+
end
126+
127+
replacement = target.replacement
128+
.gsub("%{version}", @version)
129+
.gsub("%{v_version}", "v#{@version}")
130+
131+
new_content = content.gsub(target.pattern, replacement)
132+
if new_content != content
133+
File.write(target.file_path, new_content)
134+
end
135+
true
136+
rescue ex
137+
STDERR.puts " Error updating #{target.file_path}: #{ex.message}"
138+
false
139+
end
140+
end
141+
142+
def read_shard_version : String
143+
shard_yml = YAML.parse(File.read("shard.yml"))
144+
shard_yml["version"].as_s
145+
rescue ex
146+
STDERR.puts "Error reading version from shard.yml: #{ex.message}"
147+
exit 2
148+
end
149+
150+
# Entry point
151+
show_help = ARGV.includes?("-h") || ARGV.includes?("--help")
152+
153+
if show_help
154+
puts "Usage: crystal run scripts/version_update.cr [-- NEW_VERSION]"
155+
puts ""
156+
puts "Arguments:"
157+
puts " NEW_VERSION New version to set (e.g., 0.29.0)"
158+
puts " If omitted, uses current shard.yml version"
159+
puts ""
160+
puts "Options:"
161+
puts " -h, --help Show this help message"
162+
puts ""
163+
puts "Description:"
164+
puts " Updates version across all project files using shard.yml as source of truth."
165+
puts " If NEW_VERSION is provided, shard.yml is updated first, then all other files."
166+
exit 0
167+
end
168+
169+
args = ARGV.reject(&.starts_with?("-"))
170+
if args.size > 0
171+
new_version = args[0]
172+
unless new_version.matches?(/^\d+\.\d+\.\d+$/)
173+
STDERR.puts "Error: Invalid version format '#{new_version}'. Expected: X.Y.Z"
174+
exit 2
175+
end
176+
version = new_version
177+
else
178+
version = read_shard_version
179+
end
180+
181+
updater = VersionUpdater.new(version)
182+
exit_code = updater.run
183+
exit(exit_code)

0 commit comments

Comments
 (0)