air_test 0.1.5.5 → 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 +4 -4
- data/README.md +41 -22
- 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} +44 -36
- 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 +6 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ad53fd406fa863853dea5408851219dee4651ed0f36565eea406e0e1ac8218a2
|
4
|
+
data.tar.gz: 04c60aa2b2a9db1203440e26f4d95f166826dc887865d997f9e94fe7b132bf54
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6bfe5096f73ad09e186863a51e279ab5e415d2dd3b3e09be7441fdd581634097152d249d5fe51fc802115b80081a12e25b65a93245a14694f75b1e0ca6dcbfdf
|
7
|
+
data.tar.gz: 67b13d6c175daf0360e7987e4432cd5eeb09e417bb9d2f9c3cf832926eb652be77cd14daea8dc27dd62a7284bca50aea58ef07dacc7da2546f08bc2bac7be765
|
data/README.md
CHANGED
@@ -49,6 +49,47 @@ Make sure your environment variables are set (in `.env`, your shell, or your CI/
|
|
49
49
|
|
50
50
|
---
|
51
51
|
|
52
|
+
## 📝 Creating Notion Tickets with Gherkin Format
|
53
|
+
|
54
|
+
To ensure that your Notion tickets are compatible with the air_test automation, follow these guidelines when creating your tickets:
|
55
|
+
|
56
|
+
### 1. Create a New Page in Notion
|
57
|
+
|
58
|
+
- Start by creating a new page in your Notion workspace for each ticket.
|
59
|
+
|
60
|
+
### 2. Use the Gherkin Syntax
|
61
|
+
|
62
|
+
- Each ticket should follow the Gherkin syntax, which includes the following keywords:
|
63
|
+
- **Feature**: A high-level description of the feature being implemented.
|
64
|
+
- **Scenario**: A specific situation or case that describes how the feature should behave.
|
65
|
+
- **Given**: The initial context or state before the scenario starts.
|
66
|
+
- **When**: The action that triggers the scenario.
|
67
|
+
- **Then**: The expected outcome or result of the action.
|
68
|
+
|
69
|
+
### 3. Example Structure
|
70
|
+
|
71
|
+
Here’s an example of how to structure a ticket in Notion:
|
72
|
+
|
73
|
+
Feature: User Login
|
74
|
+
Scenario: Successful login with valid credentials
|
75
|
+
Given the user is on the login page
|
76
|
+
When the user enters valid credentials
|
77
|
+
Then the user should be redirected to the dashboard
|
78
|
+
Scenario: Unsuccessful login with invalid credentials
|
79
|
+
Given the user is on the login page
|
80
|
+
When the user enters invalid credentials
|
81
|
+
Then an error message should be displayed
|
82
|
+
|
83
|
+
### 4. Additional Tips
|
84
|
+
|
85
|
+
- 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.
|
87
|
+
- Make sure to keep the Gherkin syntax consistent across all tickets for better parsing.
|
88
|
+
|
89
|
+
By following these guidelines, you can create Notion tickets that are ready to be parsed by the air_test automation tool.
|
90
|
+
|
91
|
+
---
|
92
|
+
|
52
93
|
## 🛠 Usage
|
53
94
|
|
54
95
|
Run the automated workflow from your Rails project terminal:
|
@@ -74,17 +115,6 @@ bundle exec rake air_test:generate_specs_from_notion
|
|
74
115
|
|
75
116
|
---
|
76
117
|
|
77
|
-
## 🧩 Gem Structure
|
78
|
-
|
79
|
-
- `lib/air_test/configuration.rb`: centralized configuration
|
80
|
-
- `lib/air_test/notion_parser.rb`: Notion extraction and parsing
|
81
|
-
- `lib/air_test/spec_generator.rb`: spec and step file generation
|
82
|
-
- `lib/air_test/github_client.rb`: git and GitHub PR management
|
83
|
-
- `lib/air_test/runner.rb`: workflow orchestrator
|
84
|
-
- `lib/tasks/air_test.rake`: Rake task to launch the automation
|
85
|
-
|
86
|
-
---
|
87
|
-
|
88
118
|
## 📝 Example .env
|
89
119
|
|
90
120
|
```
|
@@ -103,17 +133,6 @@ GITHUB_BOT_TOKEN=ghp_xxx
|
|
103
133
|
|
104
134
|
---
|
105
135
|
|
106
|
-
## 📦 Publishing the Gem (optional)
|
107
|
-
|
108
|
-
To publish the gem on RubyGems:
|
109
|
-
|
110
|
-
```sh
|
111
|
-
gem build air_test.gemspec
|
112
|
-
gem push air_test-x.y.z.gem
|
113
|
-
```
|
114
|
-
|
115
|
-
---
|
116
|
-
|
117
136
|
## 👨💻 Author & License
|
118
137
|
|
119
138
|
- Author: [Airtest]
|
@@ -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 :
|
7
|
+
attr_accessor :tool, :notion, :jira, :monday, :github, :repo
|
8
8
|
|
9
9
|
def initialize
|
10
|
-
@
|
11
|
-
@
|
12
|
-
|
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
|
-
@
|
9
|
+
@token = config.github[:token]
|
10
10
|
@repo = config.repo || detect_repo_from_git
|
11
|
-
@client = Octokit::Client.new(access_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 @
|
24
|
+
if @token
|
25
25
|
repo_url = "github.com/#{@repo}.git"
|
26
|
-
system("git remote set-url origin https://#{@
|
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
|
-
#
|
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)
|
@@ -26,16 +36,16 @@ module AirTest
|
|
26
36
|
|
27
37
|
def parse_ticket_content(page_id)
|
28
38
|
blocks = get_page_content(page_id)
|
29
|
-
puts "\n===== RAW NOTION BLOCKS ====="
|
30
|
-
puts JSON.pretty_generate(blocks)
|
39
|
+
# puts "\n===== RAW NOTION BLOCKS ====="
|
40
|
+
# puts JSON.pretty_generate(blocks)
|
31
41
|
return nil unless blocks
|
42
|
+
|
32
43
|
normalized_blocks = normalize_blocks(blocks)
|
33
|
-
puts "\n===== NORMALIZED BLOCKS ====="
|
34
|
-
puts JSON.pretty_generate(normalized_blocks)
|
35
|
-
|
36
|
-
puts "\n===== PARSED DATA ====="
|
37
|
-
puts JSON.pretty_generate(parsed_data)
|
38
|
-
parsed_data
|
44
|
+
# puts "\n===== NORMALIZED BLOCKS ====="
|
45
|
+
# puts JSON.pretty_generate(normalized_blocks)
|
46
|
+
parse_content(normalized_blocks)
|
47
|
+
# puts "\n===== PARSED DATA ====="
|
48
|
+
# puts JSON.pretty_generate(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.0
|
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-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/
|
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
|