air_test 0.1.5.7 ā 0.1.6.1
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.
- checksums.yaml +4 -4
- data/README.md +170 -33
- data/exe/air_test +53 -46
- data/lib/air_test/cli.rb +522 -0
- data/lib/air_test/configuration.rb +37 -4
- data/lib/air_test/github_client.rb +4 -4
- data/lib/air_test/jira_ticket_parser.rb +86 -0
- data/lib/air_test/monday_ticket_parser.rb +99 -0
- data/lib/air_test/{notion_parser.rb ā notion_ticket_parser.rb} +38 -30
- data/lib/air_test/runner.rb +21 -6
- data/lib/air_test/ticket_parser.rb +26 -0
- data/lib/air_test/version.rb +1 -1
- data/lib/air_test.rb +4 -1
- metadata +21 -3
data/lib/air_test/cli.rb
ADDED
@@ -0,0 +1,522 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'yaml'
|
4
|
+
require 'tty-prompt'
|
5
|
+
require 'fileutils'
|
6
|
+
require_relative '../air_test'
|
7
|
+
|
8
|
+
module AirTest
|
9
|
+
class CLI
|
10
|
+
def initialize
|
11
|
+
@prompt = TTY::Prompt.new
|
12
|
+
end
|
13
|
+
|
14
|
+
def init(silent: false)
|
15
|
+
puts "#{CYAN}š Initializing AirTest for your Rails project...#{RESET}\n"
|
16
|
+
|
17
|
+
if silent
|
18
|
+
# Set default values for silent mode
|
19
|
+
config = {
|
20
|
+
tool: 'notion',
|
21
|
+
auto_pr: 'no',
|
22
|
+
dev_assignee: 'default_assignee',
|
23
|
+
interactive_mode: 'no'
|
24
|
+
}
|
25
|
+
else
|
26
|
+
# Interactive prompts
|
27
|
+
config = prompt_for_configuration
|
28
|
+
end
|
29
|
+
|
30
|
+
# Create configuration files
|
31
|
+
create_airtest_yml(config)
|
32
|
+
create_initializer_file
|
33
|
+
create_env_example_file(config[:tool])
|
34
|
+
create_directories
|
35
|
+
|
36
|
+
# Check environment variables
|
37
|
+
check_environment_variables(config[:tool])
|
38
|
+
|
39
|
+
puts "\n⨠All set! Next steps:"
|
40
|
+
puts " 1. Fill in your config/initializers/air_test.rb"
|
41
|
+
puts " 2. Add your tokens to .env or your environment"
|
42
|
+
puts " 3. Run: bundle exec rake air_test:generate_specs_from_notion"
|
43
|
+
puts "\nHappy testing! š"
|
44
|
+
end
|
45
|
+
|
46
|
+
def generate(args)
|
47
|
+
puts "#{CYAN}š Generating specs from tickets...#{RESET}\n"
|
48
|
+
|
49
|
+
# Parse arguments
|
50
|
+
options = parse_generate_options(args)
|
51
|
+
|
52
|
+
# Load configuration
|
53
|
+
config = load_configuration
|
54
|
+
|
55
|
+
# Validate configuration
|
56
|
+
validate_configuration(config)
|
57
|
+
|
58
|
+
# Initialize AirTest configuration
|
59
|
+
initialize_airtest_config(config)
|
60
|
+
|
61
|
+
# Fetch tickets based on tool
|
62
|
+
tickets = fetch_tickets(config, options)
|
63
|
+
|
64
|
+
if tickets.empty?
|
65
|
+
puts "#{YELLOW}ā ļø No tickets found matching your criteria.#{RESET}"
|
66
|
+
return
|
67
|
+
end
|
68
|
+
|
69
|
+
# Handle interactive selection or search results
|
70
|
+
selected_tickets = select_tickets(tickets, options)
|
71
|
+
|
72
|
+
if selected_tickets.empty?
|
73
|
+
puts "#{YELLOW}ā ļø No tickets selected.#{RESET}"
|
74
|
+
return
|
75
|
+
end
|
76
|
+
|
77
|
+
# Process selected tickets
|
78
|
+
process_tickets(selected_tickets, config, options)
|
79
|
+
end
|
80
|
+
|
81
|
+
def create_pr(args)
|
82
|
+
puts "#{CYAN}š Creating Pull Request...#{RESET}\n"
|
83
|
+
# TODO: Implement create_pr functionality
|
84
|
+
puts "#{YELLOW}ā ļø create-pr command not yet implemented.#{RESET}"
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
def parse_generate_options(args)
|
90
|
+
options = {
|
91
|
+
interactive: false,
|
92
|
+
search: nil,
|
93
|
+
dry_run: false,
|
94
|
+
no_pr: false
|
95
|
+
}
|
96
|
+
|
97
|
+
args.each_with_index do |arg, index|
|
98
|
+
case arg
|
99
|
+
when '--interactive'
|
100
|
+
options[:interactive] = true
|
101
|
+
when '--search'
|
102
|
+
options[:search] = args[index + 1] if args[index + 1]
|
103
|
+
when '--dry-run'
|
104
|
+
options[:dry_run] = true
|
105
|
+
when '--no-pr'
|
106
|
+
options[:no_pr] = true
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
options
|
111
|
+
end
|
112
|
+
|
113
|
+
def load_configuration
|
114
|
+
unless File.exist?('.airtest.yml')
|
115
|
+
puts "#{RED}ā Configuration file .airtest.yml not found.#{RESET}"
|
116
|
+
puts "Run 'air_test init' first to set up configuration."
|
117
|
+
exit 1
|
118
|
+
end
|
119
|
+
|
120
|
+
YAML.load_file('.airtest.yml')
|
121
|
+
end
|
122
|
+
|
123
|
+
def validate_configuration(config)
|
124
|
+
tool = config['tool']
|
125
|
+
puts "#{GREEN}ā
Using #{tool.capitalize} as ticketing tool#{RESET}"
|
126
|
+
|
127
|
+
# Check required environment variables
|
128
|
+
missing_vars = []
|
129
|
+
case tool
|
130
|
+
when 'notion'
|
131
|
+
%w[NOTION_TOKEN NOTION_DATABASE_ID].each do |var|
|
132
|
+
missing_vars << var if ENV[var].nil? || ENV[var].empty?
|
133
|
+
end
|
134
|
+
when 'jira'
|
135
|
+
%w[JIRA_TOKEN JIRA_PROJECT_ID JIRA_DOMAIN JIRA_EMAIL].each do |var|
|
136
|
+
missing_vars << var if ENV[var].nil? || ENV[var].empty?
|
137
|
+
end
|
138
|
+
when 'monday'
|
139
|
+
%w[MONDAY_TOKEN MONDAY_BOARD_ID MONDAY_DOMAIN].each do |var|
|
140
|
+
missing_vars << var if ENV[var].nil? || ENV[var].empty?
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
# Always check for GitHub token
|
145
|
+
missing_vars << 'GITHUB_BOT_TOKEN' if ENV['GITHUB_BOT_TOKEN'].nil? || ENV['GITHUB_BOT_TOKEN'].empty?
|
146
|
+
|
147
|
+
if missing_vars.any?
|
148
|
+
puts "#{RED}ā Missing required environment variables: #{missing_vars.join(', ')}#{RESET}"
|
149
|
+
puts "Please set these variables in your .env file or environment."
|
150
|
+
exit 1
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
def initialize_airtest_config(config)
|
155
|
+
# Initialize AirTest configuration with environment variables
|
156
|
+
AirTest.configure do |airtest_config|
|
157
|
+
case config['tool']
|
158
|
+
when 'notion'
|
159
|
+
airtest_config.notion[:token] = ENV['NOTION_TOKEN']
|
160
|
+
airtest_config.notion[:database_id] = ENV['NOTION_DATABASE_ID']
|
161
|
+
when 'jira'
|
162
|
+
airtest_config.jira[:token] = ENV['JIRA_TOKEN']
|
163
|
+
airtest_config.jira[:project_id] = ENV['JIRA_PROJECT_ID']
|
164
|
+
airtest_config.jira[:domain] = ENV['JIRA_DOMAIN']
|
165
|
+
airtest_config.jira[:email] = ENV['JIRA_EMAIL']
|
166
|
+
when 'monday'
|
167
|
+
airtest_config.monday[:token] = ENV['MONDAY_TOKEN']
|
168
|
+
airtest_config.monday[:board_id] = ENV['MONDAY_BOARD_ID']
|
169
|
+
airtest_config.monday[:domain] = ENV['MONDAY_DOMAIN']
|
170
|
+
end
|
171
|
+
|
172
|
+
airtest_config.github[:token] = ENV['GITHUB_BOT_TOKEN']
|
173
|
+
airtest_config.repo = ENV['REPO'] || config['github']['repo']
|
174
|
+
airtest_config.tool = config['tool']
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
def fetch_tickets(config, options)
|
179
|
+
tool = config['tool']
|
180
|
+
puts "#{CYAN}š Fetching tickets from #{tool.capitalize}...#{RESET}"
|
181
|
+
|
182
|
+
# Use the existing Runner to fetch tickets
|
183
|
+
runner = AirTest::Runner.new
|
184
|
+
parser = runner.instance_variable_get(:@parser)
|
185
|
+
|
186
|
+
# Fetch all tickets (we'll filter them later)
|
187
|
+
all_tickets = parser.fetch_tickets(limit: 100)
|
188
|
+
|
189
|
+
# Filter by search if specified
|
190
|
+
if options[:search]
|
191
|
+
all_tickets = all_tickets.select { |ticket|
|
192
|
+
title = parser.extract_ticket_title(ticket)
|
193
|
+
title.downcase.include?(options[:search].downcase)
|
194
|
+
}
|
195
|
+
end
|
196
|
+
|
197
|
+
# Filter by status (only "Ready" or "Not started" tickets)
|
198
|
+
all_tickets.select { |ticket|
|
199
|
+
# This depends on the parser implementation
|
200
|
+
# For now, we'll assume all tickets are ready
|
201
|
+
true
|
202
|
+
}
|
203
|
+
end
|
204
|
+
|
205
|
+
def select_tickets(tickets, options)
|
206
|
+
if options[:interactive]
|
207
|
+
select_tickets_interactive(tickets)
|
208
|
+
else
|
209
|
+
# In non-interactive mode, process all tickets
|
210
|
+
tickets
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
def select_tickets_interactive(tickets)
|
215
|
+
puts "\n#{CYAN}Found #{tickets.length} ready tickets:#{RESET}"
|
216
|
+
tickets.each_with_index do |ticket, index|
|
217
|
+
parser = AirTest::Runner.new.instance_variable_get(:@parser)
|
218
|
+
title = parser.extract_ticket_title(ticket)
|
219
|
+
ticket_id = parser.extract_ticket_id(ticket)
|
220
|
+
puts "[#{index + 1}] #{title} (ID: #{ticket_id})"
|
221
|
+
end
|
222
|
+
|
223
|
+
puts "\nEnter numbers (comma-separated) to select tickets, or 'all' for all tickets:"
|
224
|
+
|
225
|
+
begin
|
226
|
+
selection = @prompt.ask("Selection") do |q|
|
227
|
+
q.default "all"
|
228
|
+
q.required true
|
229
|
+
end
|
230
|
+
|
231
|
+
if selection.nil? || selection.strip.empty?
|
232
|
+
puts "#{YELLOW}ā ļø No selection made, processing all tickets#{RESET}"
|
233
|
+
return tickets
|
234
|
+
end
|
235
|
+
|
236
|
+
if selection.downcase.strip == "all"
|
237
|
+
puts "#{GREEN}ā
Selected all #{tickets.length} tickets#{RESET}"
|
238
|
+
return tickets
|
239
|
+
end
|
240
|
+
|
241
|
+
selected_indices = selection.split(',').map(&:strip).map(&:to_i)
|
242
|
+
selected_tickets = selected_indices.map { |i| tickets[i - 1] }.compact
|
243
|
+
|
244
|
+
if selected_tickets.empty?
|
245
|
+
puts "#{YELLOW}ā ļø Invalid selection, processing all tickets#{RESET}"
|
246
|
+
return tickets
|
247
|
+
end
|
248
|
+
|
249
|
+
puts "#{GREEN}ā
Selected #{selected_tickets.length} tickets#{RESET}"
|
250
|
+
selected_tickets
|
251
|
+
|
252
|
+
rescue => e
|
253
|
+
puts "#{YELLOW}ā ļø Error with interactive selection: #{e.message}#{RESET}"
|
254
|
+
puts "#{YELLOW}ā ļø Processing all tickets instead#{RESET}"
|
255
|
+
return tickets
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
def process_tickets(tickets, config, options)
|
260
|
+
puts "\n#{CYAN}š Processing #{tickets.length} tickets...#{RESET}"
|
261
|
+
|
262
|
+
# Initialize the runner
|
263
|
+
runner = AirTest::Runner.new
|
264
|
+
parser = runner.instance_variable_get(:@parser)
|
265
|
+
|
266
|
+
tickets.each do |ticket|
|
267
|
+
ticket_id = parser.extract_ticket_id(ticket)
|
268
|
+
title = parser.extract_ticket_title(ticket)
|
269
|
+
url = parser.extract_ticket_url(ticket)
|
270
|
+
|
271
|
+
puts "\n#{YELLOW}š Processing: #{title} (ID: #{ticket_id})#{RESET}"
|
272
|
+
|
273
|
+
if options[:dry_run]
|
274
|
+
preview_ticket_processing(ticket, config, parser)
|
275
|
+
else
|
276
|
+
process_single_ticket(ticket, config, options, runner, parser)
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
puts "\n#{GREEN}ā
Processing complete!#{RESET}"
|
281
|
+
end
|
282
|
+
|
283
|
+
def preview_ticket_processing(ticket, config, parser)
|
284
|
+
ticket_id = parser.extract_ticket_id(ticket)
|
285
|
+
title = parser.extract_ticket_title(ticket)
|
286
|
+
url = parser.extract_ticket_url(ticket)
|
287
|
+
|
288
|
+
puts " š Ticket ID: #{ticket_id}"
|
289
|
+
puts " š Title: #{title}"
|
290
|
+
puts " š URL: #{url}"
|
291
|
+
puts " š§ Tool: #{config['tool'].capitalize}"
|
292
|
+
puts " š¤ Dev Assignee: #{config['dev_assignee']}"
|
293
|
+
puts " šæ Branch: air_test/#{ticket_id}-#{title.downcase.gsub(/[^a-z0-9]+/, '-').gsub(/^-|-$/, '')}"
|
294
|
+
puts " š Files to create:"
|
295
|
+
puts " - spec/features/[feature_slug]_fdr#{ticket_id}.rb"
|
296
|
+
puts " - spec/steps/[feature_slug]_fdr#{ticket_id}_steps.rb"
|
297
|
+
puts " š PR Title: #{title}"
|
298
|
+
end
|
299
|
+
|
300
|
+
def process_single_ticket(ticket, config, options, runner, parser)
|
301
|
+
ticket_id = parser.extract_ticket_id(ticket)
|
302
|
+
title = parser.extract_ticket_title(ticket)
|
303
|
+
url = parser.extract_ticket_url(ticket)
|
304
|
+
|
305
|
+
# Parse ticket content
|
306
|
+
parsed_data = parser.parse_ticket_content(ticket["id"])
|
307
|
+
|
308
|
+
unless parsed_data && parsed_data[:feature] && !parsed_data[:feature].empty?
|
309
|
+
puts " ā ļø Skipping ticket #{ticket_id} due to missing or empty feature."
|
310
|
+
return
|
311
|
+
end
|
312
|
+
|
313
|
+
# Generate spec files
|
314
|
+
spec_generator = runner.instance_variable_get(:@spec)
|
315
|
+
spec_path = spec_generator.generate_spec_from_parsed_data(ticket_id, parsed_data)
|
316
|
+
step_path = spec_generator.generate_step_definitions_for_spec(spec_path)
|
317
|
+
|
318
|
+
puts " ā
Generated spec files for #{title}"
|
319
|
+
|
320
|
+
# Handle Git operations and PR creation
|
321
|
+
unless options[:no_pr]
|
322
|
+
files_to_commit = [spec_path]
|
323
|
+
files_to_commit << step_path if step_path
|
324
|
+
|
325
|
+
github_client = runner.instance_variable_get(:@github)
|
326
|
+
branch = "air_test/#{ticket_id}-#{title.downcase.gsub(/[^a-z0-9]+/, '-').gsub(/^-|-$/, '')}"
|
327
|
+
|
328
|
+
has_changes = github_client.commit_and_push_branch(branch, files_to_commit, "Add specs for #{config['tool'].capitalize} ticket #{ticket_id}")
|
329
|
+
|
330
|
+
if has_changes
|
331
|
+
# Create PR
|
332
|
+
scenarios_md = parsed_data[:scenarios].map.with_index(1) do |sc, _i|
|
333
|
+
steps = sc[:steps]&.map { |step| " - #{step}" }&.join("\n")
|
334
|
+
" - [ ] #{sc[:title]}\n#{steps}"
|
335
|
+
end.join("\n")
|
336
|
+
|
337
|
+
pr_body = <<~MD
|
338
|
+
- **Story #{config['tool'].capitalize} :** #{url}
|
339
|
+
- **Feature** : #{parsed_data[:feature]}
|
340
|
+
- **ScƩnarios** :
|
341
|
+
#{scenarios_md}
|
342
|
+
- **Want to help us improve airtest?**
|
343
|
+
Leave feedback [here](http://bit.ly/4o5rinU)
|
344
|
+
or [join the community](https://discord.gg/ggnBvhtw7E)
|
345
|
+
MD
|
346
|
+
|
347
|
+
pr = github_client.create_pull_request(branch, title, pr_body)
|
348
|
+
if pr
|
349
|
+
puts " š Created PR: #{pr.html_url}"
|
350
|
+
else
|
351
|
+
puts " ā ļø Failed to create PR"
|
352
|
+
end
|
353
|
+
else
|
354
|
+
puts " ā ļø No changes detected, PR not created."
|
355
|
+
end
|
356
|
+
else
|
357
|
+
puts " ā ļø PR creation disabled (--no-pr flag)"
|
358
|
+
end
|
359
|
+
end
|
360
|
+
|
361
|
+
def prompt_for_configuration
|
362
|
+
tool = @prompt.select("Which ticketing tool do you use?", %w[notion jira monday], default: 'notion')
|
363
|
+
auto_pr = @prompt.select("Enable auto PR creation by default?", %w[yes no], default: 'no')
|
364
|
+
dev_assignee = @prompt.ask("Default dev assignee name?", default: 'default_assignee')
|
365
|
+
interactive_mode = @prompt.select("Enable interactive mode by default?", %w[yes no], default: 'no')
|
366
|
+
|
367
|
+
{
|
368
|
+
tool: tool,
|
369
|
+
auto_pr: auto_pr,
|
370
|
+
dev_assignee: dev_assignee,
|
371
|
+
interactive_mode: interactive_mode
|
372
|
+
}
|
373
|
+
end
|
374
|
+
|
375
|
+
def create_airtest_yml(config)
|
376
|
+
airtest_yml_path = '.airtest.yml'
|
377
|
+
|
378
|
+
if File.exist?(airtest_yml_path)
|
379
|
+
puts "#{YELLOW}ā ļø #{airtest_yml_path} already exists. Skipping.#{RESET}"
|
380
|
+
return
|
381
|
+
end
|
382
|
+
|
383
|
+
yaml_content = {
|
384
|
+
'tool' => config[:tool],
|
385
|
+
'auto_pr' => config[:auto_pr],
|
386
|
+
'dev_assignee' => config[:dev_assignee],
|
387
|
+
'interactive_mode' => config[:interactive_mode],
|
388
|
+
'notion' => {
|
389
|
+
'token' => 'ENV["NOTION_TOKEN"]',
|
390
|
+
'database_id' => 'ENV["NOTION_DATABASE_ID"]'
|
391
|
+
},
|
392
|
+
'jira' => {
|
393
|
+
'token' => 'ENV["JIRA_TOKEN"]',
|
394
|
+
'project_id' => 'ENV["JIRA_PROJECT_ID"]',
|
395
|
+
'domain' => 'ENV["JIRA_DOMAIN"]',
|
396
|
+
'email' => 'ENV["JIRA_EMAIL"]'
|
397
|
+
},
|
398
|
+
'monday' => {
|
399
|
+
'token' => 'ENV["MONDAY_TOKEN"]',
|
400
|
+
'board_id' => 'ENV["MONDAY_BOARD_ID"]',
|
401
|
+
'domain' => 'ENV["MONDAY_DOMAIN"]'
|
402
|
+
},
|
403
|
+
'github' => {
|
404
|
+
'token' => 'ENV["GITHUB_BOT_TOKEN"]',
|
405
|
+
'repo' => 'your-org/your-repo'
|
406
|
+
}
|
407
|
+
}
|
408
|
+
|
409
|
+
File.write(airtest_yml_path, yaml_content.to_yaml)
|
410
|
+
puts "#{GREEN}ā
Created #{airtest_yml_path}#{RESET}"
|
411
|
+
end
|
412
|
+
|
413
|
+
def create_initializer_file
|
414
|
+
initializer_path = "config/initializers/air_test.rb"
|
415
|
+
|
416
|
+
if File.exist?(initializer_path)
|
417
|
+
puts "#{YELLOW}ā ļø #{initializer_path} already exists. Skipping.#{RESET}"
|
418
|
+
return
|
419
|
+
end
|
420
|
+
|
421
|
+
FileUtils.mkdir_p(File.dirname(initializer_path))
|
422
|
+
File.write(initializer_path, <<~RUBY)
|
423
|
+
AirTest.configure do |config|
|
424
|
+
config.notion_token = ENV['NOTION_TOKEN']
|
425
|
+
config.notion_database_id = ENV['NOTION_DATABASE_ID']
|
426
|
+
config.github_token = ENV['GITHUB_BOT_TOKEN']
|
427
|
+
config.repo = 'your-org/your-repo' # format: 'organization/repo_name'
|
428
|
+
end
|
429
|
+
RUBY
|
430
|
+
puts "#{GREEN}ā
Created #{initializer_path}#{RESET}"
|
431
|
+
end
|
432
|
+
|
433
|
+
def create_env_example_file(tool)
|
434
|
+
example_env = ".env.air_test.example"
|
435
|
+
|
436
|
+
if File.exist?(example_env)
|
437
|
+
puts "#{YELLOW}ā ļø #{example_env} already exists. Skipping.#{RESET}"
|
438
|
+
return
|
439
|
+
end
|
440
|
+
|
441
|
+
env_content = case tool
|
442
|
+
when 'notion'
|
443
|
+
<<~ENV
|
444
|
+
NOTION_TOKEN=your_notion_token
|
445
|
+
NOTION_DATABASE_ID=your_notion_database_id
|
446
|
+
GITHUB_BOT_TOKEN=your_github_token
|
447
|
+
ENV
|
448
|
+
when 'jira'
|
449
|
+
<<~ENV
|
450
|
+
JIRA_TOKEN=your_jira_token
|
451
|
+
JIRA_PROJECT_ID=your_jira_project_id
|
452
|
+
JIRA_DOMAIN=your_jira_domain
|
453
|
+
JIRA_EMAIL=your_jira_email
|
454
|
+
GITHUB_BOT_TOKEN=your_github_token
|
455
|
+
ENV
|
456
|
+
when 'monday'
|
457
|
+
<<~ENV
|
458
|
+
MONDAY_TOKEN=your_monday_token
|
459
|
+
MONDAY_BOARD_ID=your_monday_board_id
|
460
|
+
MONDAY_DOMAIN=your_monday_domain
|
461
|
+
GITHUB_BOT_TOKEN=your_github_token
|
462
|
+
ENV
|
463
|
+
end
|
464
|
+
|
465
|
+
File.write(example_env, env_content)
|
466
|
+
puts "#{GREEN}ā
Created #{example_env}#{RESET}"
|
467
|
+
end
|
468
|
+
|
469
|
+
def create_directories
|
470
|
+
["spec/features", "spec/steps"].each do |dir|
|
471
|
+
if Dir.exist?(dir)
|
472
|
+
puts "#{YELLOW}ā ļø #{dir} already exists. Skipping.#{RESET}"
|
473
|
+
else
|
474
|
+
FileUtils.mkdir_p(dir)
|
475
|
+
puts "#{GREEN}ā
Created #{dir}/#{RESET}"
|
476
|
+
end
|
477
|
+
end
|
478
|
+
end
|
479
|
+
|
480
|
+
def check_environment_variables(tool)
|
481
|
+
puts "\nš Checking environment variables..."
|
482
|
+
missing = []
|
483
|
+
|
484
|
+
case tool
|
485
|
+
when 'notion'
|
486
|
+
%w[NOTION_TOKEN NOTION_DATABASE_ID GITHUB_BOT_TOKEN].each do |var|
|
487
|
+
if ENV[var].nil? || ENV[var].empty?
|
488
|
+
puts "#{YELLOW}ā ļø #{var} is not set!#{RESET}"
|
489
|
+
missing << var
|
490
|
+
else
|
491
|
+
puts "#{GREEN}ā
#{var} is set#{RESET}"
|
492
|
+
end
|
493
|
+
end
|
494
|
+
when 'jira'
|
495
|
+
%w[JIRA_TOKEN JIRA_PROJECT_ID JIRA_DOMAIN JIRA_EMAIL GITHUB_BOT_TOKEN].each do |var|
|
496
|
+
if ENV[var].nil? || ENV[var].empty?
|
497
|
+
puts "#{YELLOW}ā ļø #{var} is not set!#{RESET}"
|
498
|
+
missing << var
|
499
|
+
else
|
500
|
+
puts "#{GREEN}ā
#{var} is set#{RESET}"
|
501
|
+
end
|
502
|
+
end
|
503
|
+
when 'monday'
|
504
|
+
%w[MONDAY_TOKEN MONDAY_BOARD_ID MONDAY_DOMAIN GITHUB_BOT_TOKEN].each do |var|
|
505
|
+
if ENV[var].nil? || ENV[var].empty?
|
506
|
+
puts "#{YELLOW}ā ļø #{var} is not set!#{RESET}"
|
507
|
+
missing << var
|
508
|
+
else
|
509
|
+
puts "#{GREEN}ā
#{var} is set#{RESET}"
|
510
|
+
end
|
511
|
+
end
|
512
|
+
end
|
513
|
+
end
|
514
|
+
|
515
|
+
# Color constants
|
516
|
+
GREEN = "\e[32m"
|
517
|
+
YELLOW = "\e[33m"
|
518
|
+
RED = "\e[31m"
|
519
|
+
CYAN = "\e[36m"
|
520
|
+
RESET = "\e[0m"
|
521
|
+
end
|
522
|
+
end
|
@@ -4,13 +4,46 @@
|
|
4
4
|
module AirTest
|
5
5
|
# Handles configuration for AirTest, including API tokens and environment variables.
|
6
6
|
class Configuration
|
7
|
-
attr_accessor :
|
7
|
+
attr_accessor :tool, :notion, :jira, :monday, :github, :repo
|
8
8
|
|
9
9
|
def initialize
|
10
|
-
@
|
11
|
-
@
|
12
|
-
|
10
|
+
@tool = ENV.fetch("AIRTEST_TOOL", "notion")
|
11
|
+
@notion = {
|
12
|
+
token: ENV.fetch("NOTION_TOKEN", nil),
|
13
|
+
database_id: ENV.fetch("NOTION_DATABASE_ID", nil)
|
14
|
+
}
|
15
|
+
@jira = {
|
16
|
+
token: ENV.fetch("JIRA_TOKEN", nil),
|
17
|
+
project_id: ENV.fetch("JIRA_PROJECT_ID", nil),
|
18
|
+
domain: ENV.fetch("JIRA_DOMAIN", nil),
|
19
|
+
email: ENV.fetch("JIRA_EMAIL", nil)
|
20
|
+
}
|
21
|
+
@monday = {
|
22
|
+
token: ENV.fetch("MONDAY_TOKEN", nil),
|
23
|
+
board_id: ENV.fetch("MONDAY_BOARD_ID", nil),
|
24
|
+
domain: ENV.fetch("MONDAY_DOMAIN", nil)
|
25
|
+
}
|
26
|
+
@github = {
|
27
|
+
token: ENV["GITHUB_BOT_TOKEN"] || ENV.fetch("GITHUB_TOKEN", nil)
|
28
|
+
}
|
13
29
|
@repo = ENV.fetch("REPO", nil)
|
14
30
|
end
|
31
|
+
|
32
|
+
def validate!
|
33
|
+
case tool.to_s.downcase
|
34
|
+
when "notion"
|
35
|
+
raise "Missing NOTION_TOKEN" unless notion[:token]
|
36
|
+
raise "Missing NOTION_DATABASE_ID" unless notion[:database_id]
|
37
|
+
when "jira"
|
38
|
+
raise "Missing JIRA_TOKEN" unless jira[:token]
|
39
|
+
raise "Missing JIRA_PROJECT_ID" unless jira[:project_id]
|
40
|
+
raise "Missing JIRA_DOMAIN" unless jira[:domain]
|
41
|
+
raise "Missing JIRA_EMAIL" unless jira[:email]
|
42
|
+
when "monday"
|
43
|
+
raise "Missing MONDAY_TOKEN" unless monday[:token]
|
44
|
+
raise "Missing MONDAY_BOARD_ID" unless monday[:board_id]
|
45
|
+
raise "Missing MONDAY_DOMAIN" unless monday[:domain]
|
46
|
+
end
|
47
|
+
end
|
15
48
|
end
|
16
49
|
end
|
@@ -6,9 +6,9 @@ module AirTest
|
|
6
6
|
# Handles GitHub API interactions for AirTest, such as commits and pull requests.
|
7
7
|
class GithubClient
|
8
8
|
def initialize(config = AirTest.configuration)
|
9
|
-
@
|
9
|
+
@token = config.github[:token]
|
10
10
|
@repo = config.repo || detect_repo_from_git
|
11
|
-
@client = Octokit::Client.new(access_token: @
|
11
|
+
@client = Octokit::Client.new(access_token: @token) if @token
|
12
12
|
end
|
13
13
|
|
14
14
|
def commit_and_push_branch(branch, files, commit_message)
|
@@ -21,9 +21,9 @@ module AirTest
|
|
21
21
|
system('git config user.name "air-test-bot"')
|
22
22
|
system('git config user.email "airtest.bot@gmail.com"')
|
23
23
|
# Set remote to use bot token if available
|
24
|
-
if @
|
24
|
+
if @token
|
25
25
|
repo_url = "github.com/#{@repo}.git"
|
26
|
-
system("git remote set-url origin https://#{@
|
26
|
+
system("git remote set-url origin https://#{@token}@#{repo_url}")
|
27
27
|
end
|
28
28
|
files.each { |f| system("git add -f #{f}") }
|
29
29
|
has_changes = !system("git diff --cached --quiet")
|
@@ -0,0 +1,86 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "net/http"
|
4
|
+
require "json"
|
5
|
+
require "uri"
|
6
|
+
require_relative "ticket_parser"
|
7
|
+
|
8
|
+
module AirTest
|
9
|
+
class JiraTicketParser
|
10
|
+
include TicketParser
|
11
|
+
def initialize(config = AirTest.configuration)
|
12
|
+
@domain = config.jira[:domain]
|
13
|
+
@api_key = config.jira[:token]
|
14
|
+
@project_key = config.jira[:project_id]
|
15
|
+
@email = config.jira[:email]
|
16
|
+
end
|
17
|
+
|
18
|
+
def fetch_tickets(limit: 5)
|
19
|
+
# Try different status names (English and French)
|
20
|
+
statuses = ["To Do", "Ć faire", "Open", "New"]
|
21
|
+
all_issues = []
|
22
|
+
|
23
|
+
statuses.each do |status|
|
24
|
+
jql = "project = #{@project_key} AND status = '#{status}' ORDER BY created DESC"
|
25
|
+
uri = URI("#{@domain}/rest/api/3/search?jql=#{URI.encode_www_form_component(jql)}&maxResults=#{limit}")
|
26
|
+
request = Net::HTTP::Get.new(uri)
|
27
|
+
request.basic_auth(@email, @api_key)
|
28
|
+
request["Accept"] = "application/json"
|
29
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
30
|
+
http.use_ssl = true
|
31
|
+
response = http.request(request)
|
32
|
+
|
33
|
+
next unless response.code == "200"
|
34
|
+
|
35
|
+
data = JSON.parse(response.body)
|
36
|
+
issues = data["issues"] || []
|
37
|
+
all_issues.concat(issues)
|
38
|
+
puts "Found #{issues.length} issues with status '#{status}'" if issues.any?
|
39
|
+
end
|
40
|
+
|
41
|
+
all_issues.first(limit)
|
42
|
+
end
|
43
|
+
|
44
|
+
def parse_ticket_content(issue_id)
|
45
|
+
# Fetch issue details (description, etc.)
|
46
|
+
uri = URI("#{@domain}/rest/api/3/issue/#{issue_id}")
|
47
|
+
request = Net::HTTP::Get.new(uri)
|
48
|
+
request.basic_auth(@email, @api_key)
|
49
|
+
request["Accept"] = "application/json"
|
50
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
51
|
+
http.use_ssl = true
|
52
|
+
response = http.request(request)
|
53
|
+
return nil unless response.code == "200"
|
54
|
+
|
55
|
+
issue = JSON.parse(response.body)
|
56
|
+
# Example: parse description as feature, steps, etc. (customize as needed)
|
57
|
+
{
|
58
|
+
feature: issue.dig("fields", "summary") || "",
|
59
|
+
scenarios: [
|
60
|
+
{
|
61
|
+
title: "Scenario",
|
62
|
+
steps: [issue.dig("fields", "description", "content")&.map do |c|
|
63
|
+
c["content"]&.map do |t|
|
64
|
+
t["text"]
|
65
|
+
end&.join(" ")
|
66
|
+
end&.join(" ") || ""]
|
67
|
+
}
|
68
|
+
],
|
69
|
+
meta: { tags: [], priority: "", estimate: nil,
|
70
|
+
assignee: issue.dig("fields", "assignee", "displayName") || "" }
|
71
|
+
}
|
72
|
+
end
|
73
|
+
|
74
|
+
def extract_ticket_title(ticket)
|
75
|
+
ticket.dig("fields", "summary") || "No title"
|
76
|
+
end
|
77
|
+
|
78
|
+
def extract_ticket_id(ticket)
|
79
|
+
ticket["key"] || "No ID"
|
80
|
+
end
|
81
|
+
|
82
|
+
def extract_ticket_url(ticket)
|
83
|
+
"#{@domain}/browse/#{ticket["key"]}"
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|