air_test 0.1.6.0 → 0.1.6.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ad53fd406fa863853dea5408851219dee4651ed0f36565eea406e0e1ac8218a2
4
- data.tar.gz: 04c60aa2b2a9db1203440e26f4d95f166826dc887865d997f9e94fe7b132bf54
3
+ metadata.gz: 3e04796d0b80ac35cf63aa0d65fab417c7f1b2c793f953108be1779129489cba
4
+ data.tar.gz: 9e31c0266c59d7963c70cdc3d2158b92e12814a32be15d065f062f1026485884
5
5
  SHA512:
6
- metadata.gz: 6bfe5096f73ad09e186863a51e279ab5e415d2dd3b3e09be7441fdd581634097152d249d5fe51fc802115b80081a12e25b65a93245a14694f75b1e0ca6dcbfdf
7
- data.tar.gz: 67b13d6c175daf0360e7987e4432cd5eeb09e417bb9d2f9c3cf832926eb652be77cd14daea8dc27dd62a7284bca50aea58ef07dacc7da2546f08bc2bac7be765
6
+ metadata.gz: e6ce0f69bc475ed1ef913fac555f125c131dd49aef39f9165fffd05c8288b16d5013dfb38b41f9ed7d4119f81ba270f7b8af3ea08c4faf9c18543d8f719d5559
7
+ data.tar.gz: e4311d1981fb6289d81275619461490ac2d4ee2f1c47982fa51166dafb7e48047b4519a648d44e68c398462fe3887b63923bc326a1b72993f38c7748b542cf31
data/README.md CHANGED
@@ -1,10 +1,15 @@
1
1
  # air_test
2
2
 
3
- Automate the generation of Turnip/RSpec specs from Notion tickets, create branches, commits, pushes, and GitHub Pull Requests—all with a single Rake command.
3
+ Automate the generation of Turnip/RSpec specs from Notion tickets, create branches, commits, pushes, and GitHub Pull Requests—all with a single command.
4
4
 
5
5
  ## 🚀 Features
6
6
 
7
- - Fetches Notion tickets (Gherkin format)
7
+ - **Multi-platform support**: Works with Notion, Jira, and Monday.com
8
+ - **Interactive CLI**: Choose which tickets to process
9
+ - **Search and filtering**: Find tickets by keyword
10
+ - **Dry-run mode**: Preview changes before applying them
11
+ - **Flexible PR creation**: Generate specs with or without automatic PRs
12
+ - Fetches tickets (Gherkin format)
8
13
  - Parses and extracts features/scenarios
9
14
  - Generates Turnip/RSpec spec files and matching step definitions
10
15
  - Creates a dedicated branch, commits, pushes
@@ -31,9 +36,96 @@ bundle install
31
36
 
32
37
  ---
33
38
 
39
+ ## 🛠 Quick Start
40
+
41
+ ### 1. Initialize Configuration
42
+
43
+ Set up your project configuration interactively:
44
+
45
+ ```sh
46
+ air_test init
47
+ ```
48
+
49
+ This will:
50
+ - Ask which ticketing tool you use (Notion, Jira, or Monday)
51
+ - Configure your preferences (auto PR creation, dev assignee, etc.)
52
+ - Create `.airtest.yml` configuration file
53
+ - Create example environment file
54
+ - Set up necessary directories
55
+
56
+ For silent setup with defaults:
57
+ ```sh
58
+ air_test init --silent
59
+ ```
60
+
61
+ ### 2. Set Environment Variables
62
+
63
+ Copy the example environment file and fill in your tokens:
64
+
65
+ ```sh
66
+ cp .env.air_test.example .env
67
+ # Edit .env with your actual tokens
68
+ ```
69
+
70
+ ### 3. Generate Specs
71
+
72
+ #### Interactive Mode (Recommended)
73
+ ```sh
74
+ air_test generate --interactive
75
+ ```
76
+ Shows you a list of available tickets and lets you choose which ones to process.
77
+
78
+ #### Search and Filter
79
+ ```sh
80
+ air_test generate --search "webhook"
81
+ ```
82
+ Only processes tickets containing "webhook" in the title or content.
83
+
84
+ #### Preview Mode
85
+ ```sh
86
+ air_test generate --dry-run
87
+ ```
88
+ Shows what would be generated without creating files or PRs.
89
+
90
+ #### Disable PR Creation
91
+ ```sh
92
+ air_test generate --no-pr
93
+ ```
94
+ Generates spec files locally without creating Pull Requests.
95
+
96
+ ---
97
+
34
98
  ## ⚙️ Configuration
35
99
 
36
- Create an initializer in your Rails project:
100
+ ### CLI Configuration (`.airtest.yml`)
101
+
102
+ The `air_test init` command creates a `.airtest.yml` file with your preferences:
103
+
104
+ ```yaml
105
+ tool: notion # Your ticketing tool (notion/jira/monday)
106
+ auto_pr: 'yes' # Enable auto PR creation
107
+ dev_assignee: 'your-name' # Default dev assignee
108
+ interactive_mode: 'yes' # Enable interactive mode by default
109
+ notion:
110
+ token: ENV["NOTION_TOKEN"]
111
+ database_id: ENV["NOTION_DATABASE_ID"]
112
+ jira:
113
+ token: ENV["JIRA_TOKEN"]
114
+ project_id: ENV["JIRA_PROJECT_ID"]
115
+ domain: ENV["JIRA_DOMAIN"]
116
+ email: ENV["JIRA_EMAIL"]
117
+ monday:
118
+ token: ENV["MONDAY_TOKEN"]
119
+ board_id: ENV["MONDAY_BOARD_ID"]
120
+ domain: ENV["MONDAY_DOMAIN"]
121
+ github:
122
+ token: ENV["GITHUB_BOT_TOKEN"]
123
+ repo: 'your-org/your-repo'
124
+ ```
125
+
126
+ ### Rails Initializer (Optional)
127
+
128
+ For Rails projects, you can also create an initializer:
37
129
  `config/initializers/air_test.rb`
38
130
 
39
131
  ```ruby
@@ -45,17 +137,15 @@ AirTest.configure do |config|
45
137
  end
46
138
  ```
47
139
 
48
- Make sure your environment variables are set (in `.env`, your shell, or your CI/CD).
49
-
50
140
  ---
51
141
 
52
- ## 📝 Creating Notion Tickets with Gherkin Format
142
+ ## 📝 Creating Tickets with Gherkin Format
53
143
 
54
- To ensure that your Notion tickets are compatible with the air_test automation, follow these guidelines when creating your tickets:
144
+ To ensure that your tickets are compatible with the air_test automation, follow these guidelines when creating your tickets:
55
145
 
56
- ### 1. Create a New Page in Notion
146
+ ### 1. Create a New Page/Ticket
57
147
 
58
- - Start by creating a new page in your Notion workspace for each ticket.
148
+ - Start by creating a new page in your Notion workspace, Jira issue, or Monday.com item for each ticket.
59
149
 
60
150
  ### 2. Use the Gherkin Syntax
61
151
 
@@ -68,68 +158,117 @@ To ensure that your Notion tickets are compatible with the air_test automation,
68
158
 
69
159
  ### 3. Example Structure
70
160
 
71
- Heres an example of how to structure a ticket in Notion:
161
+ Here's an example of how to structure a ticket:
72
162
 
163
+ ```
73
164
  Feature: User Login
74
165
  Scenario: Successful login with valid credentials
75
166
  Given the user is on the login page
76
167
  When the user enters valid credentials
77
168
  Then the user should be redirected to the dashboard
169
+
78
170
  Scenario: Unsuccessful login with invalid credentials
79
171
  Given the user is on the login page
80
172
  When the user enters invalid credentials
81
173
  Then an error message should be displayed
174
+ ```
82
175
 
83
176
  ### 4. Additional Tips
84
177
 
85
178
  - Ensure that each ticket is clearly titled and contains all necessary scenarios.
86
- - Use bullet points or toggle lists in Notion to organize multiple scenarios under a single feature.
179
+ - Use bullet points or toggle lists to organize multiple scenarios under a single feature.
87
180
  - Make sure to keep the Gherkin syntax consistent across all tickets for better parsing.
88
181
 
89
- By following these guidelines, you can create Notion tickets that are ready to be parsed by the air_test automation tool.
90
-
91
182
  ---
92
183
 
93
- ## 🛠 Usage
184
+ ## 🛠 CLI Commands
185
+
186
+ ### `air_test init [--silent]`
187
+ Initialize AirTest configuration for your project.
94
188
 
95
- Run the automated workflow from your Rails project terminal:
189
+ **Options:**
190
+ - `--silent`: Use default values without prompts
96
191
 
192
+ **Examples:**
97
193
  ```sh
98
- bundle exec rake air_test:generate_specs_from_notion
194
+ air_test init # Interactive setup
195
+ air_test init --silent # Silent setup with defaults
99
196
  ```
100
197
 
101
- - This will:
102
- - Fetch Notion tickets
103
- - Generate Turnip/RSpec specs and step files
104
- - Create a branch, commit, push
105
- - Open a Pull Request on GitHub with a rich template
198
+ ### `air_test generate [options]`
199
+ Generate specs from tickets with advanced filtering and selection.
106
200
 
107
- ---
201
+ **Options:**
202
+ - `--interactive`: Interactive ticket selection
203
+ - `--search "keyword"`: Search tickets by keyword
204
+ - `--dry-run`: Preview changes without creating files
205
+ - `--no-pr`: Disable PR creation
108
206
 
109
- ## 📋 Requirements
207
+ **Examples:**
208
+ ```sh
209
+ air_test generate # Process all ready tickets
210
+ air_test generate --interactive # Choose tickets interactively
211
+ air_test generate --search "webhook" --dry-run
212
+ air_test generate --no-pr # Generate files only, no PRs
213
+ ```
110
214
 
111
- - A Notion API token with access to your database
112
- - A GitHub token with push and PR creation rights
113
- - A configured remote git repository (`git remote -v`)
114
- - The folders `spec/features` and `spec/steps` (created automatically if needed)
215
+ ### `air_test create-pr --ticket-id ID`
216
+ Create a Pull Request for a specific ticket (coming soon).
217
+
218
+ **Examples:**
219
+ ```sh
220
+ air_test create-pr --ticket-id 123
221
+ ```
222
+
223
+ ### `air_test help`
224
+ Show help information and usage examples.
115
225
 
116
226
  ---
117
227
 
118
- ## 📝 Example .env
228
+ ## 📋 Requirements
119
229
 
230
+ ### Environment Variables
231
+
232
+ #### For Notion:
120
233
  ```
121
234
  NOTION_TOKEN=secret_xxx
122
235
  NOTION_DATABASE_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
123
236
  GITHUB_BOT_TOKEN=ghp_xxx
124
237
  ```
125
238
 
239
+ #### For Jira:
240
+ ```
241
+ JIRA_TOKEN=your_jira_token
242
+ JIRA_PROJECT_ID=your_project_id
243
+ JIRA_DOMAIN=your_domain.atlassian.net
244
+ JIRA_EMAIL=your_email@example.com
245
+ GITHUB_BOT_TOKEN=ghp_xxx
246
+ ```
247
+
248
+ #### For Monday.com:
249
+ ```
250
+ MONDAY_TOKEN=your_monday_token
251
+ MONDAY_BOARD_ID=your_board_id
252
+ MONDAY_DOMAIN=your_domain.monday.com
253
+ GITHUB_BOT_TOKEN=ghp_xxx
254
+ ```
255
+
256
+ ### System Requirements
257
+
258
+ - A ticketing tool API token with access to your tickets
259
+ - A GitHub token with push and PR creation rights
260
+ - A configured remote git repository (`git remote -v`)
261
+ - The folders `spec/features` and `spec/steps` (created automatically if needed)
262
+
126
263
  ---
127
264
 
128
265
  ## 🆘 Troubleshooting
129
266
 
130
- - **Notion or GitHub authentication error**: check your tokens.
131
- - **PR not created**: make sure the branch contains commits different from `main`.
132
- - **Permission issues**: ensure the GitHub bot has access to the repo.
267
+ - **Configuration not found**: Run `air_test init` to set up configuration
268
+ - **Authentication error**: Check your API tokens in environment variables
269
+ - **No tickets found**: Verify your ticketing tool configuration and permissions
270
+ - **PR not created**: Make sure the branch contains commits different from `main`
271
+ - **Permission issues**: Ensure the GitHub bot has access to the repo
133
272
 
134
273
  ---
135
274
 
@@ -142,5 +281,3 @@ GITHUB_BOT_TOKEN=ghp_xxx
142
281
 
143
282
  **Need an integration example or install script?**
144
283
  Open an issue or contact me!
145
-
146
- ---
data/exe/air_test CHANGED
@@ -2,65 +2,72 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "fileutils"
5
+ require_relative "../lib/air_test/cli"
5
6
 
7
+ # Color constants
6
8
  GREEN = "\e[32m"
7
9
  YELLOW = "\e[33m"
8
10
  RED = "\e[31m"
9
11
  CYAN = "\e[36m"
10
12
  RESET = "\e[0m"
11
13
 
12
- puts "#{CYAN}🚀 Initializing AirTest for your Rails project...#{RESET}\n"
14
+ def show_help
15
+ puts <<~HELP
16
+ #{CYAN}AirTest CLI - Test Generation Tool#{RESET}
13
17
 
14
- initializer_path = "config/initializers/air_test.rb"
15
- if File.exist?(initializer_path)
16
- puts "#{YELLOW}⚠️ #{initializer_path} already exists. Skipping.#{RESET}"
17
- else
18
- FileUtils.mkdir_p(File.dirname(initializer_path))
19
- File.write(initializer_path, <<~RUBY)
20
- AirTest.configure do |config|
21
- config.notion_token = ENV['NOTION_TOKEN']
22
- config.notion_database_id = ENV['NOTION_DATABASE_ID']
23
- config.github_token = ENV['GITHUB_BOT_TOKEN']
24
- config.repo = 'your-org/your-repo' # format: 'organization/repo_name'
25
- end
26
- RUBY
27
- puts "#{GREEN}✅ Created #{initializer_path}#{RESET}"
28
- end
18
+ Usage: air_test [command] [options]
29
19
 
30
- ["spec/features", "spec/steps"].each do |dir|
31
- if Dir.exist?(dir)
32
- puts "#{YELLOW}⚠️ #{dir} already exists. Skipping.#{RESET}"
33
- else
34
- FileUtils.mkdir_p(dir)
35
- puts "#{GREEN}✅ Created #{dir}/#{RESET}"
36
- end
37
- end
20
+ Commands:
21
+ init [--silent] Initialize AirTest configuration
22
+ generate [options] Generate specs from tickets
23
+ create-pr [options] Create PR for specific ticket
24
+ help Show this help message
38
25
 
39
- example_env = ".env.air_test.example"
40
- if File.exist?(example_env)
41
- puts "#{YELLOW}⚠️ #{example_env} already exists. Skipping.#{RESET}"
42
- else
43
- File.write(example_env, <<~ENV)
44
- NOTION_TOKEN=your_notion_token
45
- NOTION_DATABASE_ID=your_notion_database_id
46
- GITHUB_BOT_TOKEN=your_github_token
47
- ENV
48
- puts "#{GREEN}✅ Created #{example_env}#{RESET}"
26
+ Generate Options:
27
+ --interactive Interactive ticket selection
28
+ --search "keyword" Search tickets by keyword
29
+ --dry-run Preview changes without creating files
30
+ --no-pr Disable PR creation
31
+
32
+ Create PR Options:
33
+ --ticket-id ID Ticket ID to create PR for
34
+
35
+ Global Options:
36
+ --silent Run in silent mode (use defaults)
37
+ --help Show this help message
38
+
39
+ Examples:
40
+ air_test init # Interactive initialization
41
+ air_test init --silent # Silent initialization with defaults
42
+ air_test generate --interactive # Interactive ticket selection
43
+ air_test generate --search "webhook" --dry-run
44
+ air_test create-pr --ticket-id 3 # Create PR for ticket #3
45
+ air_test help # Show this help
46
+ HELP
49
47
  end
50
48
 
51
- puts "\n🔎 Checking environment variables..."
52
- missing = []
53
- %w[NOTION_TOKEN NOTION_DATABASE_ID GITHUB_BOT_TOKEN].each do |var|
54
- if ENV[var].nil? || ENV[var].empty?
55
- puts "#{YELLOW}⚠️ #{var} is not set!#{RESET}"
56
- missing << var
49
+ def main
50
+ command = ARGV[0]
51
+ silent = ARGV.include?('--silent')
52
+
53
+ case command
54
+ when 'init'
55
+ cli = AirTest::CLI.new
56
+ cli.init(silent: silent)
57
+ when 'generate'
58
+ cli = AirTest::CLI.new
59
+ cli.generate(ARGV[1..-1])
60
+ when 'create-pr'
61
+ cli = AirTest::CLI.new
62
+ cli.create_pr(ARGV[1..-1])
63
+ when 'help', '--help', '-h', nil
64
+ show_help
57
65
  else
58
- puts "#{GREEN} #{var} is set#{RESET}"
66
+ puts "#{RED} Unknown command: #{command}#{RESET}"
67
+ puts "Run 'air_test help' for usage information."
68
+ exit 1
59
69
  end
60
70
  end
61
71
 
62
- puts "\n✨ All set! Next steps:"
63
- puts " 1. Fill in your config/initializers/air_test.rb"
64
- puts " 2. Add your tokens to .env or your environment"
65
- puts " 3. Run: bundle exec rake air_test:generate_specs_from_notion"
66
- puts "\nHappy testing! 🎉"
72
+ # Run the CLI
73
+ main
@@ -0,0 +1,552 @@
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
+ load_env_files
13
+ end
14
+
15
+ def init(silent: false)
16
+ puts "#{CYAN}🚀 Initializing AirTest for your Rails project...#{RESET}\n"
17
+
18
+ if silent
19
+ # Set default values for silent mode
20
+ config = {
21
+ tool: 'notion',
22
+ auto_pr: 'no',
23
+ dev_assignee: 'default_assignee',
24
+ interactive_mode: 'no'
25
+ }
26
+ else
27
+ # Interactive prompts
28
+ config = prompt_for_configuration
29
+ end
30
+
31
+ # Create configuration files
32
+ create_airtest_yml(config)
33
+ create_initializer_file
34
+ create_env_example_file(config[:tool])
35
+ create_directories
36
+
37
+ # Check environment variables
38
+ check_environment_variables(config[:tool])
39
+
40
+ puts "\n✨ All set! Next steps:"
41
+ puts " 1. Fill in your config/initializers/air_test.rb"
42
+ puts " 2. Add your tokens to .env or your environment"
43
+ puts " 3. Run: bundle exec rake air_test:generate_specs_from_notion"
44
+ puts "\nHappy testing! 🎉"
45
+ end
46
+
47
+ def generate(args)
48
+ puts "#{CYAN}🔍 Generating specs from tickets...#{RESET}\n"
49
+
50
+ # Parse arguments
51
+ options = parse_generate_options(args)
52
+
53
+ # Load configuration
54
+ config = load_configuration
55
+
56
+ # Validate configuration
57
+ validate_configuration(config)
58
+
59
+ # Initialize AirTest configuration
60
+ initialize_airtest_config(config)
61
+
62
+ # Fetch tickets based on tool
63
+ tickets = fetch_tickets(config, options)
64
+
65
+ if tickets.empty?
66
+ puts "#{YELLOW}⚠️ No tickets found matching your criteria.#{RESET}"
67
+ return
68
+ end
69
+
70
+ # Handle interactive selection or search results
71
+ selected_tickets = select_tickets(tickets, options)
72
+
73
+ if selected_tickets.empty?
74
+ puts "#{YELLOW}⚠️ No tickets selected.#{RESET}"
75
+ return
76
+ end
77
+
78
+ # Process selected tickets
79
+ process_tickets(selected_tickets, config, options)
80
+ end
81
+
82
+ def create_pr(args)
83
+ puts "#{CYAN}🚀 Creating Pull Request...#{RESET}\n"
84
+ # TODO: Implement create_pr functionality
85
+ puts "#{YELLOW}⚠️ create-pr command not yet implemented.#{RESET}"
86
+ end
87
+
88
+ private
89
+
90
+ def parse_generate_options(args)
91
+ options = {
92
+ interactive: false,
93
+ search: nil,
94
+ dry_run: false,
95
+ no_pr: false
96
+ }
97
+
98
+ args.each_with_index do |arg, index|
99
+ case arg
100
+ when '--interactive'
101
+ options[:interactive] = true
102
+ when '--search'
103
+ options[:search] = args[index + 1] if args[index + 1]
104
+ when '--dry-run'
105
+ options[:dry_run] = true
106
+ when '--no-pr'
107
+ options[:no_pr] = true
108
+ end
109
+ end
110
+
111
+ options
112
+ end
113
+
114
+ def load_configuration
115
+ unless File.exist?('.airtest.yml')
116
+ puts "#{RED}❌ Configuration file .airtest.yml not found.#{RESET}"
117
+ puts "Run 'air_test init' first to set up configuration."
118
+ exit 1
119
+ end
120
+
121
+ YAML.load_file('.airtest.yml')
122
+ end
123
+
124
+ def validate_configuration(config)
125
+ tool = config['tool']
126
+ puts "#{GREEN}✅ Using #{tool.capitalize} as ticketing tool#{RESET}"
127
+
128
+ # Check required environment variables
129
+ missing_vars = []
130
+ case tool
131
+ when 'notion'
132
+ %w[NOTION_TOKEN NOTION_DATABASE_ID].each do |var|
133
+ missing_vars << var if ENV[var].nil? || ENV[var].empty?
134
+ end
135
+ when 'jira'
136
+ %w[JIRA_TOKEN JIRA_PROJECT_ID JIRA_DOMAIN JIRA_EMAIL].each do |var|
137
+ missing_vars << var if ENV[var].nil? || ENV[var].empty?
138
+ end
139
+ when 'monday'
140
+ %w[MONDAY_TOKEN MONDAY_BOARD_ID MONDAY_DOMAIN].each do |var|
141
+ missing_vars << var if ENV[var].nil? || ENV[var].empty?
142
+ end
143
+ end
144
+
145
+ # Always check for GitHub token
146
+ missing_vars << 'GITHUB_BOT_TOKEN' if ENV['GITHUB_BOT_TOKEN'].nil? || ENV['GITHUB_BOT_TOKEN'].empty?
147
+
148
+ if missing_vars.any?
149
+ puts "#{RED}❌ Missing required environment variables: #{missing_vars.join(', ')}#{RESET}"
150
+ puts "Please set these variables in your .env file or environment."
151
+ exit 1
152
+ end
153
+ end
154
+
155
+ def initialize_airtest_config(config)
156
+ # Initialize AirTest configuration with values from config file
157
+ AirTest.configure do |airtest_config|
158
+ case config['tool']
159
+ when 'notion'
160
+ airtest_config.notion[:token] = config['notion']['token']
161
+ airtest_config.notion[:database_id] = config['notion']['database_id']
162
+ when 'jira'
163
+ airtest_config.jira[:token] = config['jira']['token']
164
+ airtest_config.jira[:project_id] = config['jira']['project_id']
165
+ airtest_config.jira[:domain] = config['jira']['domain']
166
+ airtest_config.jira[:email] = config['jira']['email']
167
+ when 'monday'
168
+ airtest_config.monday[:token] = config['monday']['token']
169
+ airtest_config.monday[:board_id] = config['monday']['board_id']
170
+ airtest_config.monday[:domain] = config['monday']['domain']
171
+ end
172
+
173
+ airtest_config.github[:token] = config['github']['token']
174
+ airtest_config.repo = config['github']['repo']
175
+ airtest_config.tool = config['tool']
176
+ end
177
+ end
178
+
179
+ def fetch_tickets(config, options)
180
+ tool = config['tool']
181
+ puts "#{CYAN}📋 Fetching tickets from #{tool.capitalize}...#{RESET}"
182
+
183
+ # Use the existing Runner to fetch tickets
184
+ runner = AirTest::Runner.new
185
+ parser = runner.instance_variable_get(:@parser)
186
+
187
+ # Fetch all tickets (we'll filter them later)
188
+ all_tickets = parser.fetch_tickets(limit: 100)
189
+
190
+ # Filter by search if specified
191
+ if options[:search]
192
+ all_tickets = all_tickets.select { |ticket|
193
+ title = parser.extract_ticket_title(ticket)
194
+ title.downcase.include?(options[:search].downcase)
195
+ }
196
+ end
197
+
198
+ # Filter by status (only "Ready" or "Not started" tickets)
199
+ all_tickets.select { |ticket|
200
+ # This depends on the parser implementation
201
+ # For now, we'll assume all tickets are ready
202
+ true
203
+ }
204
+ end
205
+
206
+ def select_tickets(tickets, options)
207
+ if options[:interactive]
208
+ select_tickets_interactive(tickets)
209
+ else
210
+ # In non-interactive mode, process all tickets
211
+ tickets
212
+ end
213
+ end
214
+
215
+ def select_tickets_interactive(tickets)
216
+ puts "\n#{CYAN}Found #{tickets.length} ready tickets:#{RESET}"
217
+ tickets.each_with_index do |ticket, index|
218
+ parser = AirTest::Runner.new.instance_variable_get(:@parser)
219
+ title = parser.extract_ticket_title(ticket)
220
+ ticket_id = parser.extract_ticket_id(ticket)
221
+ puts "[#{index + 1}] #{title} (ID: #{ticket_id})"
222
+ end
223
+
224
+ puts "\nEnter numbers (comma-separated) to select tickets, or 'all' for all tickets:"
225
+
226
+ begin
227
+ selection = @prompt.ask("Selection") do |q|
228
+ q.default "all"
229
+ q.required true
230
+ end
231
+
232
+ if selection.nil? || selection.strip.empty?
233
+ puts "#{YELLOW}⚠️ No selection made, processing all tickets#{RESET}"
234
+ return tickets
235
+ end
236
+
237
+ if selection.downcase.strip == "all"
238
+ puts "#{GREEN}✅ Selected all #{tickets.length} tickets#{RESET}"
239
+ return tickets
240
+ end
241
+
242
+ selected_indices = selection.split(',').map(&:strip).map(&:to_i)
243
+ selected_tickets = selected_indices.map { |i| tickets[i - 1] }.compact
244
+
245
+ if selected_tickets.empty?
246
+ puts "#{YELLOW}⚠️ Invalid selection, processing all tickets#{RESET}"
247
+ return tickets
248
+ end
249
+
250
+ puts "#{GREEN}✅ Selected #{selected_tickets.length} tickets#{RESET}"
251
+ selected_tickets
252
+
253
+ rescue => e
254
+ puts "#{YELLOW}⚠️ Error with interactive selection: #{e.message}#{RESET}"
255
+ puts "#{YELLOW}⚠️ Processing all tickets instead#{RESET}"
256
+ return tickets
257
+ end
258
+ end
259
+
260
+ def process_tickets(tickets, config, options)
261
+ puts "\n#{CYAN}🔄 Processing #{tickets.length} tickets...#{RESET}"
262
+
263
+ # Initialize the runner
264
+ runner = AirTest::Runner.new
265
+ parser = runner.instance_variable_get(:@parser)
266
+
267
+ tickets.each do |ticket|
268
+ ticket_id = parser.extract_ticket_id(ticket)
269
+ title = parser.extract_ticket_title(ticket)
270
+ url = parser.extract_ticket_url(ticket)
271
+
272
+ puts "\n#{YELLOW}📝 Processing: #{title} (ID: #{ticket_id})#{RESET}"
273
+
274
+ if options[:dry_run]
275
+ preview_ticket_processing(ticket, config, parser)
276
+ else
277
+ process_single_ticket(ticket, config, options, runner, parser)
278
+ end
279
+ end
280
+
281
+ puts "\n#{GREEN}✅ Processing complete!#{RESET}"
282
+ end
283
+
284
+ def preview_ticket_processing(ticket, config, parser)
285
+ ticket_id = parser.extract_ticket_id(ticket)
286
+ title = parser.extract_ticket_title(ticket)
287
+ url = parser.extract_ticket_url(ticket)
288
+
289
+ puts " 📋 Ticket ID: #{ticket_id}"
290
+ puts " 📝 Title: #{title}"
291
+ puts " 🔗 URL: #{url}"
292
+ puts " 🔧 Tool: #{config['tool'].capitalize}"
293
+ puts " 👤 Dev Assignee: #{config['dev_assignee']}"
294
+ puts " 🌿 Branch: air_test/#{ticket_id}-#{title.downcase.gsub(/[^a-z0-9]+/, '-').gsub(/^-|-$/, '')}"
295
+ puts " 📄 Files to create:"
296
+ puts " - spec/features/[feature_slug]_fdr#{ticket_id}.rb"
297
+ puts " - spec/steps/[feature_slug]_fdr#{ticket_id}_steps.rb"
298
+ puts " 🔗 PR Title: #{title}"
299
+ end
300
+
301
+ def process_single_ticket(ticket, config, options, runner, parser)
302
+ ticket_id = parser.extract_ticket_id(ticket)
303
+ title = parser.extract_ticket_title(ticket)
304
+ url = parser.extract_ticket_url(ticket)
305
+
306
+ # Parse ticket content
307
+ parsed_data = parser.parse_ticket_content(ticket["id"])
308
+
309
+ unless parsed_data && parsed_data[:feature] && !parsed_data[:feature].empty?
310
+ puts " ⚠️ Skipping ticket #{ticket_id} due to missing or empty feature."
311
+ return
312
+ end
313
+
314
+ # Generate spec files
315
+ spec_generator = runner.instance_variable_get(:@spec)
316
+ spec_path = spec_generator.generate_spec_from_parsed_data(ticket_id, parsed_data)
317
+ step_path = spec_generator.generate_step_definitions_for_spec(spec_path)
318
+
319
+ puts " ✅ Generated spec files for #{title}"
320
+
321
+ # Handle Git operations and PR creation
322
+ unless options[:no_pr]
323
+ files_to_commit = [spec_path]
324
+ files_to_commit << step_path if step_path
325
+
326
+ github_client = runner.instance_variable_get(:@github)
327
+ branch = "air_test/#{ticket_id}-#{title.downcase.gsub(/[^a-z0-9]+/, '-').gsub(/^-|-$/, '')}"
328
+
329
+ has_changes = github_client.commit_and_push_branch(branch, files_to_commit, "Add specs for #{config['tool'].capitalize} ticket #{ticket_id}")
330
+
331
+ if has_changes
332
+ # Create PR
333
+ scenarios_md = parsed_data[:scenarios].map.with_index(1) do |sc, _i|
334
+ steps = sc[:steps]&.map { |step| " - #{step}" }&.join("\n")
335
+ " - [ ] #{sc[:title]}\n#{steps}"
336
+ end.join("\n")
337
+
338
+ pr_body = <<~MD
339
+ - **Story #{config['tool'].capitalize} :** #{url}
340
+ - **Feature** : #{parsed_data[:feature]}
341
+ - **Scénarios** :
342
+ #{scenarios_md}
343
+ - **Want to help us improve airtest?**
344
+ Leave feedback [here](http://bit.ly/4o5rinU)
345
+ or [join the community](https://discord.gg/ggnBvhtw7E)
346
+ MD
347
+
348
+ pr = github_client.create_pull_request(branch, title, pr_body)
349
+ if pr
350
+ puts " 🔗 Created PR: #{pr.html_url}"
351
+ else
352
+ puts " ⚠️ Failed to create PR"
353
+ end
354
+ else
355
+ puts " ⚠️ No changes detected, PR not created."
356
+ end
357
+ else
358
+ puts " ⚠️ PR creation disabled (--no-pr flag)"
359
+ end
360
+ end
361
+
362
+ def prompt_for_configuration
363
+ tool = @prompt.select("Which ticketing tool do you use?", %w[notion jira monday], default: 'notion')
364
+ auto_pr = @prompt.select("Enable auto PR creation by default?", %w[yes no], default: 'no')
365
+ dev_assignee = @prompt.ask("Default dev assignee name?", default: 'default_assignee')
366
+ interactive_mode = @prompt.select("Enable interactive mode by default?", %w[yes no], default: 'no')
367
+
368
+ {
369
+ tool: tool,
370
+ auto_pr: auto_pr,
371
+ dev_assignee: dev_assignee,
372
+ interactive_mode: interactive_mode
373
+ }
374
+ end
375
+
376
+ def create_airtest_yml(config)
377
+ airtest_yml_path = '.airtest.yml'
378
+
379
+ if File.exist?(airtest_yml_path)
380
+ puts "#{YELLOW}⚠️ #{airtest_yml_path} already exists. Skipping.#{RESET}"
381
+ return
382
+ end
383
+
384
+ yaml_content = {
385
+ 'tool' => config[:tool],
386
+ 'auto_pr' => config[:auto_pr],
387
+ 'dev_assignee' => config[:dev_assignee],
388
+ 'interactive_mode' => config[:interactive_mode],
389
+ 'notion' => {
390
+ 'token' => ENV['NOTION_TOKEN'] || 'your_notion_token',
391
+ 'database_id' => ENV['NOTION_DATABASE_ID'] || 'your_notion_database_id'
392
+ },
393
+ 'jira' => {
394
+ 'token' => ENV['JIRA_TOKEN'] || 'your_jira_token',
395
+ 'project_id' => ENV['JIRA_PROJECT_ID'] || 'your_jira_project_id',
396
+ 'domain' => ENV['JIRA_DOMAIN'] || 'your_jira_domain',
397
+ 'email' => ENV['JIRA_EMAIL'] || 'your_jira_email'
398
+ },
399
+ 'monday' => {
400
+ 'token' => ENV['MONDAY_TOKEN'] || 'your_monday_token',
401
+ 'board_id' => ENV['MONDAY_BOARD_ID'] || 'your_monday_board_id',
402
+ 'domain' => ENV['MONDAY_DOMAIN'] || 'your_monday_domain'
403
+ },
404
+ 'github' => {
405
+ 'token' => ENV['GITHUB_BOT_TOKEN'] || 'your_github_token',
406
+ 'repo' => ENV['REPO'] || 'your-org/your-repo'
407
+ }
408
+ }
409
+
410
+ File.write(airtest_yml_path, yaml_content.to_yaml)
411
+ puts "#{GREEN}✅ Created #{airtest_yml_path}#{RESET}"
412
+ end
413
+
414
+ def create_initializer_file
415
+ initializer_path = "config/initializers/air_test.rb"
416
+
417
+ if File.exist?(initializer_path)
418
+ puts "#{YELLOW}⚠️ #{initializer_path} already exists. Skipping.#{RESET}"
419
+ return
420
+ end
421
+
422
+ FileUtils.mkdir_p(File.dirname(initializer_path))
423
+ File.write(initializer_path, <<~RUBY)
424
+ AirTest.configure do |config|
425
+ config.notion_token = ENV['NOTION_TOKEN']
426
+ config.notion_database_id = ENV['NOTION_DATABASE_ID']
427
+ config.github_token = ENV['GITHUB_BOT_TOKEN']
428
+ config.repo = 'your-org/your-repo' # format: 'organization/repo_name'
429
+ end
430
+ RUBY
431
+ puts "#{GREEN}✅ Created #{initializer_path}#{RESET}"
432
+ end
433
+
434
+ def create_env_example_file(tool)
435
+ example_env = ".env.air_test.example"
436
+
437
+ if File.exist?(example_env)
438
+ puts "#{YELLOW}⚠️ #{example_env} already exists. Skipping.#{RESET}"
439
+ return
440
+ end
441
+
442
+ env_content = case tool
443
+ when 'notion'
444
+ <<~ENV
445
+ NOTION_TOKEN=your_notion_token
446
+ NOTION_DATABASE_ID=your_notion_database_id
447
+ GITHUB_BOT_TOKEN=your_github_token
448
+ ENV
449
+ when 'jira'
450
+ <<~ENV
451
+ JIRA_TOKEN=your_jira_token
452
+ JIRA_PROJECT_ID=your_jira_project_id
453
+ JIRA_DOMAIN=your_jira_domain
454
+ JIRA_EMAIL=your_jira_email
455
+ GITHUB_BOT_TOKEN=your_github_token
456
+ ENV
457
+ when 'monday'
458
+ <<~ENV
459
+ MONDAY_TOKEN=your_monday_token
460
+ MONDAY_BOARD_ID=your_monday_board_id
461
+ MONDAY_DOMAIN=your_monday_domain
462
+ GITHUB_BOT_TOKEN=your_github_token
463
+ ENV
464
+ end
465
+
466
+ File.write(example_env, env_content)
467
+ puts "#{GREEN}✅ Created #{example_env}#{RESET}"
468
+ end
469
+
470
+ def create_directories
471
+ ["spec/features", "spec/steps"].each do |dir|
472
+ if Dir.exist?(dir)
473
+ puts "#{YELLOW}⚠️ #{dir} already exists. Skipping.#{RESET}"
474
+ else
475
+ FileUtils.mkdir_p(dir)
476
+ puts "#{GREEN}✅ Created #{dir}/#{RESET}"
477
+ end
478
+ end
479
+ end
480
+
481
+ def check_environment_variables(tool)
482
+ puts "\n🔎 Checking environment variables..."
483
+ missing = []
484
+
485
+ case tool
486
+ when 'notion'
487
+ %w[NOTION_TOKEN NOTION_DATABASE_ID GITHUB_BOT_TOKEN].each do |var|
488
+ if ENV[var].nil? || ENV[var].empty?
489
+ puts "#{YELLOW}⚠️ #{var} is not set!#{RESET}"
490
+ missing << var
491
+ else
492
+ puts "#{GREEN}✅ #{var} is set#{RESET}"
493
+ end
494
+ end
495
+ when 'jira'
496
+ %w[JIRA_TOKEN JIRA_PROJECT_ID JIRA_DOMAIN JIRA_EMAIL GITHUB_BOT_TOKEN].each do |var|
497
+ if ENV[var].nil? || ENV[var].empty?
498
+ puts "#{YELLOW}⚠️ #{var} is not set!#{RESET}"
499
+ missing << var
500
+ else
501
+ puts "#{GREEN}✅ #{var} is set#{RESET}"
502
+ end
503
+ end
504
+ when 'monday'
505
+ %w[MONDAY_TOKEN MONDAY_BOARD_ID MONDAY_DOMAIN GITHUB_BOT_TOKEN].each do |var|
506
+ if ENV[var].nil? || ENV[var].empty?
507
+ puts "#{YELLOW}⚠️ #{var} is not set!#{RESET}"
508
+ missing << var
509
+ else
510
+ puts "#{GREEN}✅ #{var} is set#{RESET}"
511
+ end
512
+ end
513
+ end
514
+ end
515
+
516
+ private
517
+
518
+ def load_env_files
519
+ # Try to load .env files in order of preference
520
+ env_files = ['.env.airtest', '.env']
521
+
522
+ env_files.each do |env_file|
523
+ if File.exist?(env_file)
524
+ load_env_file(env_file)
525
+ puts "#{GREEN}✅ Loaded environment variables from #{env_file}#{RESET}" if ENV['AIRTEST_DEBUG']
526
+ break
527
+ end
528
+ end
529
+ end
530
+
531
+ def load_env_file(file_path)
532
+ File.readlines(file_path).each do |line|
533
+ line.strip!
534
+ next if line.empty? || line.start_with?('#')
535
+
536
+ if line.include?('=')
537
+ key, value = line.split('=', 2)
538
+ ENV[key.strip] = value.strip.gsub(/^["']|["']$/, '') # Remove quotes
539
+ end
540
+ end
541
+ rescue => e
542
+ puts "#{YELLOW}⚠️ Warning: Could not load #{file_path}: #{e.message}#{RESET}" if ENV['AIRTEST_DEBUG']
543
+ end
544
+
545
+ # Color constants
546
+ GREEN = "\e[32m"
547
+ YELLOW = "\e[33m"
548
+ RED = "\e[31m"
549
+ CYAN = "\e[36m"
550
+ RESET = "\e[0m"
551
+ end
552
+ end
@@ -187,7 +187,7 @@ module AirTest
187
187
  end
188
188
  if %w[heading_1 heading_2 heading_3].include?(block_type)
189
189
  heading_text = text.strip
190
- in_steps = heading_text.downcase.include?("feature")) || heading_text.downcase.include?("scenario")
190
+ in_steps = heading_text.downcase.include?("feature") || heading_text.downcase.include?("scenario")
191
191
  elsif %w[paragraph bulleted_list_item numbered_list_item].include?(block_type)
192
192
  steps << text if in_steps && !text.empty? && !text.strip.downcase.start_with?("scenario:")
193
193
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AirTest
4
- VERSION = "0.1.6.0"
4
+ VERSION = "0.1.6.2"
5
5
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: air_test
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.6.0
4
+ version: 0.1.6.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - julien bouland
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-07-28 00:00:00.000000000 Z
10
+ date: 2025-07-29 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: rails
@@ -51,6 +51,20 @@ dependencies:
51
51
  - - "~>"
52
52
  - !ruby/object:Gem::Version
53
53
  version: '7.0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: tty-prompt
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '0.23'
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '0.23'
54
68
  description: Automate the generation of Turnip/RSpec specs from Notion tickets, create
55
69
  branches, commits, pushes, and GitHub Pull Requests. All with a single Rake command.
56
70
  email:
@@ -69,6 +83,7 @@ files:
69
83
  - Rakefile
70
84
  - exe/air_test
71
85
  - lib/air_test.rb
86
+ - lib/air_test/cli.rb
72
87
  - lib/air_test/configuration.rb
73
88
  - lib/air_test/engine.rb
74
89
  - lib/air_test/github_client.rb