air_test 0.1.5.7 → 0.1.6.0

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: 8174f558c0b41c33acc6d4a468ad38c36e621c188a6ea022b7dc2ae500fcee3c
4
- data.tar.gz: d84cc877b0841689de2f62e7055b68955fcf7352bbda10f65cc60969f959a1ac
3
+ metadata.gz: ad53fd406fa863853dea5408851219dee4651ed0f36565eea406e0e1ac8218a2
4
+ data.tar.gz: 04c60aa2b2a9db1203440e26f4d95f166826dc887865d997f9e94fe7b132bf54
5
5
  SHA512:
6
- metadata.gz: f2089f266b3606af5c05f5153305a56e1e695391642c3b5775c81ba55b8d4752748628d74ec6e30f3a3b0df0d02cd2aca856fcf8b8d4350eaaa0fc3833b9cb44
7
- data.tar.gz: c41e1ca2dfeb4e694126276a262dded5e62e1e0252024ecca5a6a9cbcb46a39d61e80754a0488cfee83b4e844604bba1362ad790e1f009b83df30f48b7e75b1f
6
+ metadata.gz: 6bfe5096f73ad09e186863a51e279ab5e415d2dd3b3e09be7441fdd581634097152d249d5fe51fc802115b80081a12e25b65a93245a14694f75b1e0ca6dcbfdf
7
+ data.tar.gz: 67b13d6c175daf0360e7987e4432cd5eeb09e417bb9d2f9c3cf832926eb652be77cd14daea8dc27dd62a7284bca50aea58ef07dacc7da2546f08bc2bac7be765
@@ -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
@@ -0,0 +1,99 @@
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 MondayTicketParser
10
+ include TicketParser
11
+ def initialize(config = AirTest.configuration)
12
+ @api_token = config.monday[:token]
13
+ @board_id = config.monday[:board_id]
14
+ @domain = config.monday[:domain]
15
+ @base_url = "https://api.monday.com/v2"
16
+ end
17
+
18
+ def fetch_tickets(limit: 5)
19
+ # First, get all items from the board
20
+ query = <<~GRAPHQL
21
+ query {
22
+ boards(ids: "#{@board_id}") {
23
+ items_page {
24
+ items {
25
+ id
26
+ name
27
+ column_values {
28
+ id
29
+ text
30
+ value
31
+ }
32
+ }
33
+ }
34
+ }
35
+ }
36
+ GRAPHQL
37
+
38
+ response = make_graphql_request(query)
39
+ return [] unless response["data"]
40
+
41
+ items = response.dig("data", "boards", 0, "items_page", "items") || []
42
+
43
+ # Filter for items with "Not Started" status
44
+ not_started_items = items.select do |item|
45
+ status_column = item["column_values"].find { |cv| cv["id"] == "project_status" }
46
+ status_column && status_column["text"] == "Not Started"
47
+ end
48
+
49
+ not_started_items.first(limit)
50
+ end
51
+
52
+ def parse_ticket_content(item_id)
53
+ # For Monday, we'll use the item name as feature and create a simple scenario
54
+ # In the future, you could add a description column to Monday and parse it like Notion
55
+ {
56
+ feature: "Feature: #{extract_ticket_title({ "id" => item_id, "name" => "Loading..." })}",
57
+ scenarios: [
58
+ {
59
+ title: "Scenario",
60
+ steps: ["Implement the feature"]
61
+ }
62
+ ],
63
+ meta: { tags: [], priority: "", estimate: nil, assignee: "" }
64
+ }
65
+ end
66
+
67
+ def extract_ticket_title(ticket)
68
+ ticket["name"] || "No title"
69
+ end
70
+
71
+ def extract_ticket_id(ticket)
72
+ ticket["id"] || "No ID"
73
+ end
74
+
75
+ def extract_ticket_url(ticket)
76
+ "https://#{@domain}/boards/#{@board_id}/pulses/#{ticket["id"]}"
77
+ end
78
+
79
+ private
80
+
81
+ def make_graphql_request(query)
82
+ uri = URI(@base_url)
83
+ request = Net::HTTP::Post.new(uri)
84
+ request["Authorization"] = @api_token
85
+ request["Content-Type"] = "application/json"
86
+ request.body = { query: query }.to_json
87
+
88
+ http = Net::HTTP.new(uri.host, uri.port)
89
+ http.use_ssl = true
90
+ response = http.request(request)
91
+
92
+ return {} unless response.code == "200"
93
+
94
+ JSON.parse(response.body)
95
+ rescue JSON::ParserError
96
+ {}
97
+ end
98
+ end
99
+ end
@@ -1,23 +1,33 @@
1
- # Parses Notion tickets and extracts relevant information for spec generation in AirTest.
2
- # rubocop:disable Metrics/ClassLength
3
1
  # frozen_string_literal: true
4
2
 
5
3
  require "net/http"
6
4
  require "json"
7
5
  require "uri"
6
+ require_relative "ticket_parser"
8
7
 
9
8
  module AirTest
10
- # Parses Notion tickets and extracts relevant information for spec generation in AirTest.
11
- class NotionParser
9
+ # Implements TicketParser for Notion integration.
10
+ class NotionTicketParser
11
+ include TicketParser
12
12
  def initialize(config = AirTest.configuration)
13
- @database_id = config.notion_database_id
14
- @notion_token = config.notion_token
13
+ @database_id = config.notion[:database_id]
14
+ @notion_token = config.notion[:token]
15
15
  @base_url = "https://api.notion.com/v1"
16
16
  end
17
17
 
18
18
  def fetch_tickets(limit: 5)
19
19
  uri = URI("#{@base_url}/databases/#{@database_id}/query")
20
- response = make_api_request(uri, { page_size: 100 })
20
+ # Add filter for 'Not started' status
21
+ request_body = {
22
+ page_size: 100,
23
+ filter: {
24
+ property: "Status",
25
+ select: {
26
+ equals: "Not started"
27
+ }
28
+ }
29
+ }
30
+ response = make_api_request(uri, request_body)
21
31
  return [] unless response.code == "200"
22
32
 
23
33
  data = JSON.parse(response.body)
@@ -29,13 +39,13 @@ module AirTest
29
39
  # puts "\n===== RAW NOTION BLOCKS ====="
30
40
  # puts JSON.pretty_generate(blocks)
31
41
  return nil unless blocks
42
+
32
43
  normalized_blocks = normalize_blocks(blocks)
33
44
  # puts "\n===== NORMALIZED BLOCKS ====="
34
45
  # puts JSON.pretty_generate(normalized_blocks)
35
- parsed_data = parse_content(normalized_blocks)
46
+ parse_content(normalized_blocks)
36
47
  # puts "\n===== PARSED DATA ====="
37
48
  # puts JSON.pretty_generate(parsed_data)
38
- parsed_data
39
49
  end
40
50
 
41
51
  def extract_ticket_title(ticket)
@@ -93,7 +103,11 @@ module AirTest
93
103
 
94
104
  blocks.each do |block|
95
105
  block_type = block["type"]
96
- text = extract_text(block[block_type]["rich_text"]) rescue ""
106
+ text = begin
107
+ extract_text(block[block_type]["rich_text"])
108
+ rescue StandardError
109
+ ""
110
+ end
97
111
 
98
112
  if %w[heading_1 heading_2 heading_3].include?(block_type)
99
113
  heading_text = text.strip
@@ -127,19 +141,19 @@ module AirTest
127
141
  parsed_data[:scenarios] << current_scenario
128
142
  elsif %w[paragraph bulleted_list_item numbered_list_item].include?(block_type)
129
143
  next if text.empty?
144
+
130
145
  if in_feature_block
131
146
  parsed_data[:feature] += "\n#{text}"
132
147
  elsif in_background_block
133
148
  background_steps << text
134
149
  elsif in_scenario_block && current_scenario
135
150
  # Only add as step if not a scenario heading
136
- unless text.strip.downcase.start_with?("scenario:")
137
- current_scenario[:steps] << text
138
- end
151
+ current_scenario[:steps] << text unless text.strip.downcase.start_with?("scenario:")
139
152
  end
140
153
  elsif block_type == "callout"
141
154
  text = extract_text(block["callout"]["rich_text"])
142
155
  next if text.empty?
156
+
143
157
  if text.downcase.include?("tag")
144
158
  tags = extract_tags(text)
145
159
  parsed_data[:meta][:tags].concat(tags)
@@ -166,23 +180,19 @@ module AirTest
166
180
  in_steps = false
167
181
  blocks.each do |block|
168
182
  block_type = block["type"]
169
- text = extract_text(block[block_type]["rich_text"] || block["paragraph"]["rich_text"]) rescue ""
183
+ text = begin
184
+ extract_text(block[block_type]["rich_text"] || block["paragraph"]["rich_text"])
185
+ rescue StandardError
186
+ ""
187
+ end
170
188
  if %w[heading_1 heading_2 heading_3].include?(block_type)
171
189
  heading_text = text.strip
172
- if heading_text.downcase.include?("feature")
173
- in_steps = true
174
- elsif heading_text.downcase.include?("scenario")
175
- in_steps = true
176
- else
177
- in_steps = false
178
- end
190
+ in_steps = heading_text.downcase.include?("feature")) || heading_text.downcase.include?("scenario")
179
191
  elsif %w[paragraph bulleted_list_item numbered_list_item].include?(block_type)
180
192
  steps << text if in_steps && !text.empty? && !text.strip.downcase.start_with?("scenario:")
181
193
  end
182
194
  end
183
- if steps.any?
184
- parsed_data[:scenarios] << { title: "Scenario", steps: steps }
185
- end
195
+ parsed_data[:scenarios] << { title: "Scenario", steps: steps } if steps.any?
186
196
  end
187
197
 
188
198
  parsed_data[:feature] = parsed_data[:feature].strip
@@ -234,10 +244,10 @@ module AirTest
234
244
  blocks.each do |block|
235
245
  block_type = block["type"]
236
246
  text = if block[block_type] && block[block_type]["rich_text"]
237
- block[block_type]["rich_text"].map { |rt| rt["plain_text"] }.join("")
238
- else
239
- ""
240
- end
247
+ block[block_type]["rich_text"].map { |rt| rt["plain_text"] }.join
248
+ else
249
+ ""
250
+ end
241
251
  lines = text.split("\n").map(&:strip).reject(&:empty?)
242
252
  if lines.size > 1
243
253
  lines.each do |line|
@@ -254,5 +264,3 @@ module AirTest
254
264
  end
255
265
  end
256
266
  end
257
-
258
- # rubocop:enable Metrics/ClassLength
@@ -6,20 +6,32 @@ module AirTest
6
6
  # Runs the main automation workflow for AirTest, orchestrating Notion parsing and GitHub actions.
7
7
  class Runner
8
8
  def initialize(config = AirTest.configuration)
9
- @notion = NotionParser.new(config)
9
+ @config = config
10
+ @parser = case config.tool.to_s.downcase
11
+ when "notion"
12
+ NotionTicketParser.new(config)
13
+ when "jira"
14
+ JiraTicketParser.new(config)
15
+ when "monday"
16
+ MondayTicketParser.new(config)
17
+ else
18
+ raise "Unknown tool: #{config.tool}"
19
+ end
10
20
  @spec = SpecGenerator.new
11
21
  @github = GithubClient.new(config)
12
22
  end
13
23
 
14
24
  def run(limit: 5)
15
- tickets = @notion.fetch_tickets(limit: limit)
25
+ @config.validate!
26
+ tickets = @parser.fetch_tickets(limit: limit)
27
+ # Filter for 'Not started' tickets (assuming each parser returns only those, or filter here if needed)
16
28
  puts "🔍 Found #{tickets.length} tickets"
17
29
  tickets.each do |ticket|
18
- ticket_id = @notion.extract_ticket_id(ticket)
19
- title = @notion.extract_ticket_title(ticket)
20
- url = @notion.extract_ticket_url(ticket)
30
+ ticket_id = @parser.extract_ticket_id(ticket)
31
+ title = @parser.extract_ticket_title(ticket)
32
+ url = @parser.extract_ticket_url(ticket)
21
33
  puts "\n📋 Processing: FDR#{ticket_id} - #{title}"
22
- parsed_data = @notion.parse_ticket_content(ticket["id"])
34
+ parsed_data = @parser.parse_ticket_content(ticket["id"])
23
35
  unless parsed_data && parsed_data[:feature] && !parsed_data[:feature].empty?
24
36
  puts "⚠️ Skipping ticket FDR#{ticket_id} due to missing or empty feature."
25
37
  next
@@ -43,6 +55,9 @@ module AirTest
43
55
  - **Feature** : #{parsed_data[:feature]}
44
56
  - **Scénarios** :
45
57
  #{scenarios_md}
58
+ - **Want to help us improve airtest?**
59
+ Leave feedback [here](http://bit.ly/4o5rinU)
60
+ or [join the community](https://discord.gg/ggnBvhtw7E)
46
61
  MD
47
62
  pr = @github.create_pull_request(branch, pr_title, pr_body)
48
63
  puts "✅ Pull Request créée : #{pr.html_url}" if pr
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AirTest
4
+ # Interface for ticket parsers (Notion, Jira, Monday, etc.)
5
+ module TicketParser
6
+ def fetch_tickets(limit: 5)
7
+ raise NotImplementedError
8
+ end
9
+
10
+ def parse_ticket_content(page_id)
11
+ raise NotImplementedError
12
+ end
13
+
14
+ def extract_ticket_title(ticket)
15
+ raise NotImplementedError
16
+ end
17
+
18
+ def extract_ticket_id(ticket)
19
+ raise NotImplementedError
20
+ end
21
+
22
+ def extract_ticket_url(ticket)
23
+ raise NotImplementedError
24
+ end
25
+ end
26
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AirTest
4
- VERSION = "0.1.5.7"
4
+ VERSION = "0.1.6.0"
5
5
  end
data/lib/air_test.rb CHANGED
@@ -14,7 +14,10 @@ end
14
14
 
15
15
  require_relative "air_test/version"
16
16
  require_relative "air_test/configuration"
17
- require_relative "air_test/notion_parser"
17
+ require_relative "air_test/ticket_parser"
18
+ require_relative "air_test/notion_ticket_parser"
19
+ require_relative "air_test/jira_ticket_parser"
20
+ require_relative "air_test/monday_ticket_parser"
18
21
  require_relative "air_test/spec_generator"
19
22
  require_relative "air_test/github_client"
20
23
  require_relative "air_test/runner"
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.5.7
4
+ version: 0.1.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - julien bouland
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-07-23 00:00:00.000000000 Z
10
+ date: 2025-07-28 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: rails
@@ -72,10 +72,13 @@ files:
72
72
  - lib/air_test/configuration.rb
73
73
  - lib/air_test/engine.rb
74
74
  - lib/air_test/github_client.rb
75
- - lib/air_test/notion_parser.rb
75
+ - lib/air_test/jira_ticket_parser.rb
76
+ - lib/air_test/monday_ticket_parser.rb
77
+ - lib/air_test/notion_ticket_parser.rb
76
78
  - lib/air_test/runner.rb
77
79
  - lib/air_test/spec_generator.rb
78
80
  - lib/air_test/tasks/air_test.rake
81
+ - lib/air_test/ticket_parser.rb
79
82
  - lib/air_test/version.rb
80
83
  - sig/air_test.rbs
81
84
  homepage: https://github.com/airtest-dev/airtest