-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathrakefile.rb
executable file
·458 lines (376 loc) · 14.3 KB
/
rakefile.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
# =============================================================================
#
# MODULE : rakefile.rb
# PROJECT : go-testpredicate
# DESCRIPTION :
#
# Copyright (c) 2016-2021, Marc-Antoine Argenton. All rights reserved.
# =============================================================================
require 'fileutils'
task default: [:build]
desc 'Display build information'
task :info do
summary = {
"Module" => GoBuild.default.gomod,
"Version" => GoBuild.default.version,
"Source" => File.join(BuildInfo.default.remote, "tree", BuildInfo.default.commit[0,10]),
}
if GoBuild.default.targets.count > 0 then
summary["Main target"] = GoBuild.default.main_target
if GoBuild.default.targets.count > 1 then
targets = (GoBuild.default.targets.keys - [GoBuild.default.main_target])
summary["Additional targets"] = targets
end
end
# record_summary("## Build summary\n\n#{format_summary_table(summary)}\n")
print_summary(summary, prefixes: {
"Main target" => "build/bin",
"Additional targets" => "build/bin",
})
export_summary("## Build summary\n\n#{format_summary_table(summary)}\n")
export_env("VERSION=#{GoBuild.default.version}")
end
desc 'Display inferred build version string'
task :version do
puts GoBuild.default.version
end
desc 'Run all tests and capture results'
task :test => [:info] do
FileUtils.makedirs( ['./build/artifacts'] )
success = go_test()
go_testreport('build/go-test-result.json',
'--md-shift-headers=1',
'-oyaml=build/artifacts/test-report.yaml',
'-omd=build/artifacts/test-report.md',
'-omdsf=build/go-test-summary.md',
'-omdsfd=build/go-test-details.md',
)
puts File.read('build/go-test-summary.md')
export_summary(File.read('build/go-test-details.md'))
exit(1) if !success
end
desc 'Build and publish both release archive and associated container image'
task :build => [:info, :test] do
FileUtils.makedirs( './build/bin' )
GoBuild.default.commands.each do |name, cmd|
puts "Building #{name} ..."
puts cmd
system( cmd)
exit $?.exitstatus if $?.exitstatus != 0
end
generate_release_notes()
end
desc 'Remove build artifacts'
task :clean do
FileUtils.rm_rf('./build')
end
def generate_release_notes()
version = BuildInfo.default.version
File.write( 'build/release_notes.md', extract_release_notes(version,
# prefix: "go-testreport",
input:'RELEASES.md',
))
end
# ----------------------------------------------------------------------------
# BuildInfo : Helper to extract version inforrmation for git repo
# ----------------------------------------------------------------------------
class BuildInfo
class << self
def default() return @default ||= new end
end
def initialize()
if _git('rev-parse --is-shallow-repository') == 'true'
puts "Fetching missing information from remote ..."
system(' git fetch --prune --tags --unshallow')
end
end
def name() return @name ||= _name() end
def version() return @version ||= _version() end
def remote() return @remote ||= _remote() end
def commit() return @commit ||= _commit() end
def dir() return @dir ||= _dir() end
private
def _windows?() return RUBY_PLATFORM =~ /win32|mingw|mswin/ end
def _dev_null() return _windows? ? "NUL" : "/dev/null" end
def _git( cmd ) return `git #{cmd} 2>#{_dev_null}`.strip() end
def _commit() return _git('rev-parse HEAD') end
def _dir() return _git('rev-parse --show-toplevel') end
def _name()
remote_basename = File.basename(remote() || "" )
return remote_basename if remote_basename != ""
return File.basename(File.expand_path("."))
end
def _version()
v, b, n, g = _info() # Extract base info from git branch and tags
m = _mtag() # Detect locally modified files
v = _patch(v) if n > 0 || !m.nil? # Increment patch if needed to to preserve semver orderring
b = 'rc' if _is_default_branch(b, v) # Rename branch to 'rc' for default branch
return v if b == 'rc' && n == 0 && m.nil?
return "#{v}-" + [b, n, g, m].compact().join('.')
end
def _info()
# Note: Due to glob(7) limitations, the following pattern enforces
# 3-part dot-separated sequences starting with a digit,
# rather than 3 dot-separated numbers.
d = _git("describe --always --tags --long --match \"v[0-9]*.[0-9]*.[0-9]*\"").strip.split('-')
if d.count != 0
b = _git("rev-parse --abbrev-ref HEAD").strip.gsub(/[^A-Za-z0-9\._-]+/, '-')
return ['v0.0.0', b, _git("rev-list --count HEAD").strip.to_i, "g#{d[0]}"] if d.count == 1
return [d[0], b, d[1].to_i, d[2]] if d.count == 3
end
return ['v0.0.0', "none", 0, 'g0000000']
end
def _is_default_branch(b, v)
# Check branch name against common main branch names, and branch name
# that matches the beginning of the version strings e.g. 'v1' is
# considered a default branch for version 'v1.x.y'.
return ["main", "master", "HEAD"].include?(b) ||
(!v.nil? && v.start_with?(b))
end
def _patch(v)
# Increment the patch number by 1, so that intermediate version strings
# sort between the last tag and the next tag according to semver.
# v0.6.1
# v0.6.1-maa-cleanup.1.g6ede8cd <-- with _patch()
# v0.6.0
# v0.6.0-maa-cleanup.1.g6ede8cd <-- without _patch()
# v0.5.99
vv = v[1..-1].split('.').map { |v| v.to_i }
vv[-1] += 1
v = "v" + vv.join(".")
return v
end
def _mtag()
# Generate a `.mXXXXXXXX` fragment based on latest mtime of modified
# files in the index. Returns `nil` if no files are locally modified.
status = _git("status --porcelain=2 --untracked-files=no")
files = status.lines.map {|l| l.strip.split(/ +/).last }.map { |n| n.split(/\t/).first }
t = files.map { |f| File.mtime(f).to_i rescue nil }.compact.max
return t.nil? ? nil : "m%08x" % t
end
GIT_SSH_REPO = /git@(?<host>[^:]+):(?<path>.+).git/
def _remote()
remote = _git('remote get-url origin')
m = GIT_SSH_REPO.match(remote)
return remote if m.nil?
host = m[:host]
host = "github.com" if host.end_with? ("github.com")
return "https://#{host}/#{m[:path]}/"
end
end
# ----------------------------------------------------------------------------
# GoBuild : Helper to build go projects
# ----------------------------------------------------------------------------
class GoBuild
class << self
def default() return @default ||= new end
end
def initialize( buildinfo = nil )
@buildinfo = buildinfo || BuildInfo.default
end
def gomod() return @gomod ||= _gomod() end
def targets() return @tagets ||= _targets() end
def main_target() return @main_target ||= _main_target() end
def version() return @version ||= @buildinfo.version end
def ldflags() return @ldflags ||= _ldflags() end
def commands(action = 'build')
flags = %Q{"#{ldflags}"}
Hash[targets.map do |name, input|
output = File.join( './build/bin', name )
cmd = [ "go #{action} -trimpath -ldflags #{flags}",
("-o #{output}" if action == 'build'),
"#{input}"
].compact.join(' ')
[name, cmd]
end]
end
private
def _gomod()
return '' if !File.readable?('go.mod')
File.foreach('go.mod') do |l|
return l[7..-1].strip if l.start_with?( 'module ' )
end
end
def _targets()
targets = Hash[Dir["./cmd/**/main.go"].map do |f|
path = File.dirname(f)
[File.basename(path), File.join( path, "..." )]
end]
targets[File.basename(gomod)] = "." if File.exist?("./main.go")
targets
end
def _ldflags()
prefix = "#{gomod}/pkg/buildinfo"
{ Version: @buildinfo.version,
GitHash: @buildinfo.commit,
GitRepo: @buildinfo.remote,
BuildRoot: @buildinfo.dir
}.map { |k,v| "-X #{prefix}.#{k}=#{v}"}.join(' ')
end
def _main_target()
mod = File.basename(gomod)
targets.keys.min_by{ |v| _lev(v, mod)}
end
def _lev(a, b, memo={})
return b.size if a.empty?
return a.size if b.empty?
return memo[[a, b]] ||= [
_lev(a.chop, b, memo) + 1,
_lev(a, b.chop, memo) + 1,
_lev(a.chop, b.chop, memo) + (a[-1] == b[-1] ? 0 : 1)
].min
end
end
def go_test()
FileUtils.makedirs( ['./build'] )
cmd = "go test -race " +
"-coverprofile=build/go-test-coverage.txt -covermode=atomic " +
"-json ./... > build/go-test-result.json"
puts cmd
system(cmd)
end
def go_testreport(*args)
cmd = %w{go run github.com/maargenton/[email protected]}
cmd += args
puts cmd
system(*cmd)
end
# ----------------------------------------------------------------------------
# DockerHelper : Helper to build go projects
# ----------------------------------------------------------------------------
def docker_registry_tags(base_tag)
return [github_registry_tag(base_tag)]
end
def github_registry_tag(base_tag)
return if ENV['GITHUB_ACTOR'].nil? || ENV['GITHUB_REPOSITORY'].nil?
if ENV['GITHUB_TOKEN'].nil? then
puts "Found GitHub Actiona context but no 'GITHUB_TOKEN'."
puts "Image will not be pushed to GitHub package registry."
puts "To resolve this issue, add the following to your workflow:"
puts " env:"
puts " GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}"
return
end
# Authenticate
puts "Authenticating with docker.pkg.github.com..."
system("echo ${GITHUB_TOKEN} | docker login ghcr.io --username ${GITHUB_ACTOR} --password-stdin")
puts "Failed to authenticate with docker.pkg.github.com" if $?.exitstatus != 0
return File.join('ghcr.io', ENV['GITHUB_REPOSITORY_OWNER'], base_tag)
end
# ----------------------------------------------------------------------------
# Github Actions integration
# ----------------------------------------------------------------------------
def export_summary(content)
return if ENV['GITHUB_STEP_SUMMARY'].nil?
summary_filename = ENV['GITHUB_STEP_SUMMARY']
open(summary_filename, 'a') do |f|
f.puts content
end
end
def export_env(env)
return if ENV['GITHUB_ENV'].nil?
env_filename = ENV['GITHUB_ENV']
open(env_filename, 'a') do |f|
f.puts env
end
end
# ----------------------------------------------------------------------------
# Build summary generator
# ----------------------------------------------------------------------------
def print_summary(summary, prefixes: {})
def _single_value(value)
if value.respond_to?(:each) && value.respond_to?(:count)
return nil if value.count > 1
return value[0]
end
return value
end
def _format_value(value, prefix)
return File.join(prefix, value) if prefix
return value
end
puts "---"
width = summary.select { |k,v| !_single_value(v).nil? }.map { |k,v| k.length }.max
summary.each do |k,v|
prefix = prefixes[k]
vv = _single_value(v)
if vv.nil?
puts "#{k}:"
puts v.map { |t| " - #{_format_value(t, prefix)}" }.join(" \n")
else
puts "#{(k+':').ljust(width+1)} #{_format_value(vv, prefix)}"
end
end
puts "---"
end
def format_summary_table(summary)
o = "| | |\n|-|-|\n"
summary.each do |key, value|
if value.respond_to?('each')
value.each_with_index do |v, i|
o += (i == 0) ? "| #{key} | `#{v}`\n" : "| | `#{v}`\n"
end
else
o += "| #{key} | `#{value}`\n"
end
end
return o
end
# ----------------------------------------------------------------------------
# Release notes generator
# ----------------------------------------------------------------------------
def extract_release_notes(version, prefix:nil, input:nil, checksums:nil)
rn = ""
rn += "#{prefix} #{version}\n\n" if prefix
rn += load_release_notes(input, version) if input
rn += "\n## Checksums\n\n```\n" + File.read(checksums) + "```\n" if checksums
rn
end
def load_release_notes(filename, version)
notes, capture = [], false
File.readlines(filename).each do |l|
if l.start_with?( "# ")
break if capture
capture = true if version.start_with?(l[2..-1].strip())
elsif capture
notes << l
end
end
notes.shift while (notes.first || "-").strip == ""
return notes.join()
end
# ----------------------------------------------------------------------------
# Definitions to help formating 'rake watch' results
# ----------------------------------------------------------------------------
TERM_WIDTH = `tput cols`.to_i || 80
def tty_red(str); "\e[31m#{str}\e[0m" end
def tty_green(str); "\e[32m#{str}\e[0m" end
def tty_blink(str); "\e[5m#{str}\e[25m" end
def tty_reverse_color(str); "\e[7m#{str}\e[27m" end
def print_separator( success = true )
if success
puts tty_green( "-" * TERM_WIDTH )
else
puts tty_reverse_color(tty_red( "-" * TERM_WIDTH ))
end
end
# ----------------------------------------------------------------------------
# Definition of watch task, that monitors the project folder for any relevant
# file change and runs the unit test of the project.
# ----------------------------------------------------------------------------
def watch( *glob )
yield unless block_given?
files = []
loop do
new_files = Dir[*glob].map {|file| File.mtime(file) }
yield if new_files != files
files = new_files
sleep 0.5
end
end
# task :watch do
# watch( '**/*.{c,cc,cpp,h,hh,hpp,ld}', 'Makefile' ) do
# success = system "clear && rake"
# print_separator( success )
# end
# end