-
Notifications
You must be signed in to change notification settings - Fork 3
/
Rakefile
240 lines (212 loc) · 7.19 KB
/
Rakefile
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
# frozen_string_literal: true
# To display all the available rake (Ruby make) tasks, run:
# rake -T
require 'date'
require 'json'
require 'net/http'
require 'optparse'
require 'rake/testtask'
# To run all tests:
# rake (or) rake test
#
# To run one test file:
# rake test TEST=test/FILENAME
#
# To run an individual test in a test file:
# rake test TEST=test/FILENAME TESTOPTS=--name=TEST_NAME
#
desc 'Run all tests with `rake` or `rake test`'
task default: :test # Make test runner the default rake task.
Rake::TestTask.new do |task|
task.pattern = 'test/test_*.rb'
end
# These correspond to the GitHub labels used by Bitcoin Core.
DESIRED_COMPONENTS = [
'Block storage',
'Build system',
'Consensus',
'Data corruption',
'Descriptors',
'Docs',
'GUI',
'Interfaces',
'Mempool',
'Mining',
'P2P',
'Privacy',
'PSBT',
'Refactoring',
'Resource usage',
'RPC/REST/ZMQ',
'Scripts and tools',
'Settings',
'Tests',
'TX fees and policy',
'Utils/log/libs',
'UTXO Db and Indexes',
'Validation',
'Wallet',
].freeze
COMPONENTS = DESIRED_COMPONENTS.map(&:downcase).freeze
# Some PRs contain undesired words (here, single characters) immediately after
# the prefix. Run `rake -- posts:new --host username --pr 16729` for an example.
# Characters or words we want removed after the prefix can go into this array.
UNDESIRED_PR_TITLE_WORDS = %w(- _).freeze
GITHUB_API_URL = 'https://api.github.com/repos/lightningnetwork/lnd/pulls'
HTTP_SUCCESS = '200'
HTTP_NOTFOUND = '404'
HTTP_ERRORS = [
JSON::ParserError,
SocketError,
EOFError,
IOError,
Errno::ECONNRESET,
Errno::EINVAL,
Net::HTTPBadResponse,
Net::HTTPHeaderSyntaxError,
Net::ProtocolError,
Timeout::Error
].freeze
# To see the rake posts:new help, run:
# rake posts:new -- -H
#
desc 'Create a new post file'
namespace :posts do
task :new do
# Fetch user command line args. Exit if required args are missing.
pr, host, date = get_cli_options
handle_missing_required_arg('pr') unless pr
handle_missing_required_arg('host') unless host
# Ensure PR contains only numerical characters.
unless pr.size == pr.gsub(/[^0-9-]/i, '').size
puts "Error: Non-numerical PR #{pr} received. Nothing done, exiting."
exit
end
# Set default value for meeting date if not supplied by user.
unless date
date = next_wednesday.to_s
puts "Date set to next Wednesday: #{date}"
end
# Ensure meeting date is valid.
unless valid_iso8601_date?(date)
puts "Error: Invalid date (#{date} received, YYYY-MM-DD needed). Nothing done, exiting."
exit
end
# Fetch pull request data from the GitHub v3 REST API.
http = Net::HTTP.get_response(URI("#{GITHUB_API_URL}/#{pr}"))
response = parse_response(http, pr)
# Create a new post file if none exists, otherwise exit.
filename = "#{'_posts/' if File.directory?('_posts')}#{date}-##{pr}.md"
if File.file?(filename)
puts "Filename #{filename} already exists. Nothing done, exiting."
else
create_post_file!(filename, response, date, host)
puts "New file #{filename} created successfully."
end
exit
end
end
def get_cli_options(options = {})
OptionParser.new do |opts|
opts.banner = 'Usage: rake posts:new -- <options>'
opts.on('-p', '--pr NUMBER', 'Pull request number (required)') do
|pr| options[:pr] = pr
end
opts.on('-h', '--host USERNAME', "Host's GitHub username (required)") do
|host| options[:host] = host
end
opts.on('-d', '--date YYYY-MM-DD',
'Meeting date in ISO8601 format (optional, defaults to next Wednesday)') do
|date| options[:date] = date
end
opts.on('-H', '--help', 'Display this help') do
puts opts
exit
end
args = opts.order!(ARGV) {}
opts.parse!(args)
end
[options[:pr], options[:host], options[:date]]
end
def handle_missing_required_arg(name)
puts "Error: Missing required --#{name} argument. Run `rake posts:new -- --help` for info."
exit
end
def valid_iso8601_date?(date_string)
yyyy, mm, dd = date_string.split('-').map(&:to_i)
date_string.size == date_string.gsub(/[^0-9-]/i, '').size &&
[yyyy, mm, dd].none?(nil) && [yyyy, mm, dd].none?(0) &&
yyyy >= Date.today.year && Date.valid_date?(yyyy, mm, dd)
end
def next_wednesday(date = Date.today, wednesday = 3)
date + ((wednesday - date.wday) % 7)
end
def parse_response(http, pr)
code, msg, body = http.code, http.message, http.body
if code == HTTP_SUCCESS
JSON.parse(body)
else
puts "Error: HTTP #{code} #{msg}#{". PR #{pr} doesn't exist" if code == HTTP_NOTFOUND}."
exit
end
rescue *HTTP_ERRORS => e
"Error #{e.inspect}"
end
def create_post_file!(filename, response, date, host)
title = parse_title(response['title'])
components = parse_valid_components(response['labels'])
puts "GitHub PR title: \"#{response['title']}\""
puts "Parsed PR title: #{title}"
puts "GitHub PR labels: \"#{parse_components(response['labels']).join(', ')}\""
puts "Parsed PR labels: \"#{components.join(', ')}\""
File.open(filename, 'w') do |line|
line.puts '---'
line.puts 'layout: pr'
line.puts "date: #{date}"
line.puts "title: #{title}"
line.puts "pr: #{response['number']}"
line.puts "authors: [#{response.dig('user', 'login')}]"
line.puts "components: #{components}"
line.puts "host: #{host}"
line.puts "status: upcoming"
line.puts "commit:"
line.puts "---\n\n"
line.puts "_Notes and questions to follow soon!_\n\n"
line.puts "<!-- TODO: Before meeting, add notes and questions"
line.puts "## Notes\n\n"
line.puts "## Questions"
line.puts "1. Did you review the PR? [Concept ACK, approach ACK, tested ACK, or NACK](https://github.com/lightningnetwork/lnd/blob/master/docs/code_contribution_guidelines.md#code-approval-process)?"
line.puts "-->\n\n\n"
line.puts "<!-- TODO: After meeting, uncomment and add meeting log between the irc tags"
line.puts "## Meeting Log\n\n"
line.puts "{% irc %}"
line.puts "{% endirc %}"
line.puts "-->"
end
end
def parse_title(title)
first, *rest = title.split # e.g. if title = "a b c", first = "a", rest = ["b", "c"]
first.downcase! # mutate first word to lowercase in place
rest.shift if UNDESIRED_PR_TITLE_WORDS.include?(rest[0]) # rm 1st word if undesired
prefix = first.gsub(/[:\[\]]/, '') # prefix is first word stripped of :[] chars
# If prefix is different from first word and is a component, drop first word.
words = if first != prefix && is_a_component?(prefix)
[rest.first&.capitalize] + rest[1..-1]
else
[first&.capitalize] + rest
end
# Return enclosed in double quotes after joining words and removing any double quotes.
"\"#{words.join(' ').gsub(/"/, '')}\""
end
def is_a_component?(prefix)
# Boolean indicating whether `prefix` (without any final "s") is a component.
# Iterates through the COMPONENTS array and exits with true at the first
# instance where `prefix` is a substring of the component; otherwise false.
COMPONENTS.any? { |component| component.include?(prefix.chomp('s')) }
end
def parse_components(labels)
(labels.map { |label| label['name'].downcase })
end
def parse_valid_components(labels)
parse_components(labels) & COMPONENTS
end