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.
@@ -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 :notion_token, :notion_database_id, :github_token, :repo
7
+ attr_accessor :tool, :notion, :jira, :monday, :github, :repo
8
8
 
9
9
  def initialize
10
- @notion_token = ENV.fetch("NOTION_TOKEN", nil)
11
- @notion_database_id = ENV.fetch("NOTION_DATABASE_ID", nil)
12
- @github_token = ENV["GITHUB_BOT_TOKEN"] || ENV.fetch("GITHUB_TOKEN", nil)
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
- @github_token = config.github_token
9
+ @token = config.github[:token]
10
10
  @repo = config.repo || detect_repo_from_git
11
- @client = Octokit::Client.new(access_token: @github_token) if @github_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 @github_token
24
+ if @token
25
25
  repo_url = "github.com/#{@repo}.git"
26
- system("git remote set-url origin https://#{@github_token}@#{repo_url}")
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