air_test 0.1.6.0 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ad53fd406fa863853dea5408851219dee4651ed0f36565eea406e0e1ac8218a2
4
- data.tar.gz: 04c60aa2b2a9db1203440e26f4d95f166826dc887865d997f9e94fe7b132bf54
3
+ metadata.gz: 76572704e0d1a8c3abc7e274def6c51611efba858a6f434b6bbfd5f871aedf12
4
+ data.tar.gz: 11235311464e8fe0db11323d3f0fee3fb156c676ab4b3b56f7c02abf410a71d0
5
5
  SHA512:
6
- metadata.gz: 6bfe5096f73ad09e186863a51e279ab5e415d2dd3b3e09be7441fdd581634097152d249d5fe51fc802115b80081a12e25b65a93245a14694f75b1e0ca6dcbfdf
7
- data.tar.gz: 67b13d6c175daf0360e7987e4432cd5eeb09e417bb9d2f9c3cf832926eb652be77cd14daea8dc27dd62a7284bca50aea58ef07dacc7da2546f08bc2bac7be765
6
+ metadata.gz: 793c1fd212fcbd747c85abd0130f69dafa849035d4ce69595b72342f119c73f69c9be5f88fe8375e9c439a8be12e96c487f7e93f733f36957163df49d3647d8a
7
+ data.tar.gz: dca493fc7c39853ae784be49e4b187e3d8b752b2d0e7306f2532b4b7d71584e185130f922527598b2e07e64633a256173a5c1fb8dca2053aedcc7a53eac9ff6d
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,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
@@ -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.1"
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.1
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