air_test 0.1.5.7 → 0.1.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +170 -33
- data/exe/air_test +53 -46
- data/lib/air_test/cli.rb +522 -0
- data/lib/air_test/configuration.rb +37 -4
- data/lib/air_test/github_client.rb +4 -4
- data/lib/air_test/jira_ticket_parser.rb +86 -0
- data/lib/air_test/monday_ticket_parser.rb +99 -0
- data/lib/air_test/{notion_parser.rb → notion_ticket_parser.rb} +38 -30
- data/lib/air_test/runner.rb +21 -6
- data/lib/air_test/ticket_parser.rb +26 -0
- data/lib/air_test/version.rb +1 -1
- data/lib/air_test.rb +4 -1
- metadata +21 -3
@@ -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
|
-
#
|
11
|
-
class
|
9
|
+
# Implements TicketParser for Notion integration.
|
10
|
+
class NotionTicketParser
|
11
|
+
include TicketParser
|
12
12
|
def initialize(config = AirTest.configuration)
|
13
|
-
@database_id = config.
|
14
|
-
@notion_token = config.
|
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
|
-
|
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
|
-
|
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 =
|
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 =
|
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
|
-
|
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
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
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
|
data/lib/air_test/runner.rb
CHANGED
@@ -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
|
-
@
|
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
|
-
|
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 = @
|
19
|
-
title = @
|
20
|
-
url = @
|
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 = @
|
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
|
data/lib/air_test/version.rb
CHANGED
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/
|
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.
|
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-
|
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,13 +83,17 @@ 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
|
75
|
-
- lib/air_test/
|
90
|
+
- lib/air_test/jira_ticket_parser.rb
|
91
|
+
- lib/air_test/monday_ticket_parser.rb
|
92
|
+
- lib/air_test/notion_ticket_parser.rb
|
76
93
|
- lib/air_test/runner.rb
|
77
94
|
- lib/air_test/spec_generator.rb
|
78
95
|
- lib/air_test/tasks/air_test.rake
|
96
|
+
- lib/air_test/ticket_parser.rb
|
79
97
|
- lib/air_test/version.rb
|
80
98
|
- sig/air_test.rbs
|
81
99
|
homepage: https://github.com/airtest-dev/airtest
|