qai-cli 3.0.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.
- package/.claude/mcp-config.json +12 -0
- package/.claude/qa-engineer-prompt.md +194 -0
- package/.eslintrc.json +69 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +79 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +50 -0
- package/.github/ISSUE_TEMPLATE/security.md +43 -0
- package/.github/dependabot.yml +51 -0
- package/.github/pull_request_template.md +11 -0
- package/.github/workflows/lint.yml +35 -0
- package/.github/workflows/playwright-qa.yml +223 -0
- package/.github/workflows/qa-engineer.yml +309 -0
- package/.github/workflows/visual-regression.yml +192 -0
- package/.prettierrc.json +10 -0
- package/README.md +111 -0
- package/action.yml +149 -0
- package/docs/BUGS.md +43 -0
- package/docs/app.js +101 -0
- package/docs/index.html +129 -0
- package/docs/style.css +315 -0
- package/examples/workflow-local.yml +22 -0
- package/examples/workflow-with-vercel.yml +40 -0
- package/package.json +83 -0
- package/qa-report-agent.md +30 -0
- package/qa-report-kudos.md +35 -0
- package/scripts/aria-snapshot.js +328 -0
- package/scripts/page-utils.js +357 -0
- package/scripts/visual-regression.cjs +339 -0
- package/src/analyze.js +365 -0
- package/src/capture.js +133 -0
- package/src/index.js +204 -0
- package/src/providers/anthropic.js +59 -0
- package/src/providers/base.js +164 -0
- package/src/providers/gemini.js +42 -0
- package/src/providers/index.js +132 -0
- package/src/providers/ollama.js +49 -0
- package/src/providers/openai.js +54 -0
- package/src/types.d.ts +148 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
name: Playwright + AI QA
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_call:
|
|
5
|
+
inputs:
|
|
6
|
+
working_directory:
|
|
7
|
+
description: 'Directory containing playwright.config (default: repo root)'
|
|
8
|
+
required: false
|
|
9
|
+
type: string
|
|
10
|
+
default: '.'
|
|
11
|
+
test_command:
|
|
12
|
+
description: 'Command to run tests (default: npx playwright test)'
|
|
13
|
+
required: false
|
|
14
|
+
type: string
|
|
15
|
+
default: 'npx playwright test'
|
|
16
|
+
preview_url:
|
|
17
|
+
description: 'Preview URL to test against (optional - uses baseURL from config if not provided)'
|
|
18
|
+
required: false
|
|
19
|
+
type: string
|
|
20
|
+
node_version:
|
|
21
|
+
description: 'Node.js version'
|
|
22
|
+
required: false
|
|
23
|
+
type: string
|
|
24
|
+
default: '20'
|
|
25
|
+
secrets:
|
|
26
|
+
ANTHROPIC_API_KEY:
|
|
27
|
+
required: false
|
|
28
|
+
OPENAI_API_KEY:
|
|
29
|
+
required: false
|
|
30
|
+
|
|
31
|
+
jobs:
|
|
32
|
+
playwright-qa:
|
|
33
|
+
name: Playwright + AI QA
|
|
34
|
+
runs-on: ubuntu-latest
|
|
35
|
+
timeout-minutes: 30
|
|
36
|
+
permissions:
|
|
37
|
+
contents: read
|
|
38
|
+
pull-requests: write
|
|
39
|
+
actions: read
|
|
40
|
+
|
|
41
|
+
steps:
|
|
42
|
+
- name: Checkout repository
|
|
43
|
+
uses: actions/checkout@v4
|
|
44
|
+
|
|
45
|
+
- name: Setup Node.js
|
|
46
|
+
uses: actions/setup-node@v4
|
|
47
|
+
with:
|
|
48
|
+
node-version: ${{ inputs.node_version }}
|
|
49
|
+
cache: 'npm'
|
|
50
|
+
cache-dependency-path: ${{ inputs.working_directory }}/package-lock.json
|
|
51
|
+
|
|
52
|
+
- name: Install dependencies
|
|
53
|
+
run: npm ci
|
|
54
|
+
working-directory: ${{ inputs.working_directory }}
|
|
55
|
+
|
|
56
|
+
- name: Install Playwright browsers
|
|
57
|
+
run: npx playwright install --with-deps chromium
|
|
58
|
+
working-directory: ${{ inputs.working_directory }}
|
|
59
|
+
|
|
60
|
+
- name: Run Playwright tests
|
|
61
|
+
id: playwright
|
|
62
|
+
run: |
|
|
63
|
+
# Run tests with HTML reporter, continue even if tests fail
|
|
64
|
+
${{ inputs.test_command }} --reporter=html,json || true
|
|
65
|
+
|
|
66
|
+
# Check if report was generated
|
|
67
|
+
if [ -d "playwright-report" ]; then
|
|
68
|
+
echo "report_exists=true" >> $GITHUB_OUTPUT
|
|
69
|
+
else
|
|
70
|
+
echo "report_exists=false" >> $GITHUB_OUTPUT
|
|
71
|
+
fi
|
|
72
|
+
|
|
73
|
+
# Parse test results if JSON exists
|
|
74
|
+
if [ -f "test-results.json" ]; then
|
|
75
|
+
total=$(jq '.stats.expected + .stats.unexpected + .stats.flaky + .stats.skipped' test-results.json 2>/dev/null || echo "0")
|
|
76
|
+
passed=$(jq '.stats.expected' test-results.json 2>/dev/null || echo "0")
|
|
77
|
+
failed=$(jq '.stats.unexpected' test-results.json 2>/dev/null || echo "0")
|
|
78
|
+
flaky=$(jq '.stats.flaky' test-results.json 2>/dev/null || echo "0")
|
|
79
|
+
skipped=$(jq '.stats.skipped' test-results.json 2>/dev/null || echo "0")
|
|
80
|
+
|
|
81
|
+
echo "total=$total" >> $GITHUB_OUTPUT
|
|
82
|
+
echo "passed=$passed" >> $GITHUB_OUTPUT
|
|
83
|
+
echo "failed=$failed" >> $GITHUB_OUTPUT
|
|
84
|
+
echo "flaky=$flaky" >> $GITHUB_OUTPUT
|
|
85
|
+
echo "skipped=$skipped" >> $GITHUB_OUTPUT
|
|
86
|
+
fi
|
|
87
|
+
working-directory: ${{ inputs.working_directory }}
|
|
88
|
+
env:
|
|
89
|
+
PLAYWRIGHT_BASE_URL: ${{ inputs.preview_url }}
|
|
90
|
+
|
|
91
|
+
- name: Checkout qaie
|
|
92
|
+
uses: actions/checkout@v4
|
|
93
|
+
with:
|
|
94
|
+
repository: tyler-james-bridges/qaie
|
|
95
|
+
ref: main
|
|
96
|
+
path: _qaie
|
|
97
|
+
|
|
98
|
+
- name: Setup qaie
|
|
99
|
+
run: |
|
|
100
|
+
cd _qaie
|
|
101
|
+
npm ci
|
|
102
|
+
npx playwright install chromium
|
|
103
|
+
|
|
104
|
+
- name: Run AI QA Analysis
|
|
105
|
+
id: qai
|
|
106
|
+
if: ${{ inputs.preview_url != '' }}
|
|
107
|
+
run: |
|
|
108
|
+
cd _qaie
|
|
109
|
+
node src/index.js
|
|
110
|
+
env:
|
|
111
|
+
URL: ${{ inputs.preview_url }}
|
|
112
|
+
VIEWPORTS: desktop,mobile
|
|
113
|
+
FOCUS: all
|
|
114
|
+
OUTPUT_FORMAT: markdown
|
|
115
|
+
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
116
|
+
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
|
117
|
+
continue-on-error: true
|
|
118
|
+
|
|
119
|
+
- name: Upload Playwright Report
|
|
120
|
+
uses: actions/upload-artifact@v4
|
|
121
|
+
if: always()
|
|
122
|
+
with:
|
|
123
|
+
name: playwright-report
|
|
124
|
+
path: ${{ inputs.working_directory }}/playwright-report/
|
|
125
|
+
retention-days: 14
|
|
126
|
+
|
|
127
|
+
- name: Upload AI QA Screenshots
|
|
128
|
+
uses: actions/upload-artifact@v4
|
|
129
|
+
if: always() && inputs.preview_url != ''
|
|
130
|
+
with:
|
|
131
|
+
name: qai-screenshots
|
|
132
|
+
path: _qaie/screenshots/
|
|
133
|
+
retention-days: 14
|
|
134
|
+
|
|
135
|
+
- name: Post Results to PR
|
|
136
|
+
if: github.event_name == 'pull_request'
|
|
137
|
+
uses: actions/github-script@v7
|
|
138
|
+
with:
|
|
139
|
+
script: |
|
|
140
|
+
const fs = require('fs');
|
|
141
|
+
|
|
142
|
+
// Build comment
|
|
143
|
+
let comment = '## 🎭 Playwright + AI QA Report\n\n';
|
|
144
|
+
|
|
145
|
+
// Playwright results
|
|
146
|
+
const total = '${{ steps.playwright.outputs.total }}' || '0';
|
|
147
|
+
const passed = '${{ steps.playwright.outputs.passed }}' || '0';
|
|
148
|
+
const failed = '${{ steps.playwright.outputs.failed }}' || '0';
|
|
149
|
+
const flaky = '${{ steps.playwright.outputs.flaky }}' || '0';
|
|
150
|
+
const skipped = '${{ steps.playwright.outputs.skipped }}' || '0';
|
|
151
|
+
|
|
152
|
+
comment += '### 🧪 Playwright Test Results\n\n';
|
|
153
|
+
|
|
154
|
+
if (total !== '0') {
|
|
155
|
+
const passRate = ((parseInt(passed) / parseInt(total)) * 100).toFixed(1);
|
|
156
|
+
const statusEmoji = failed === '0' ? '✅' : '❌';
|
|
157
|
+
|
|
158
|
+
comment += `| Status | Count |\n`;
|
|
159
|
+
comment += `|--------|-------|\n`;
|
|
160
|
+
comment += `| ${statusEmoji} Passed | ${passed} |\n`;
|
|
161
|
+
if (failed !== '0') comment += `| ❌ Failed | ${failed} |\n`;
|
|
162
|
+
if (flaky !== '0') comment += `| ⚠️ Flaky | ${flaky} |\n`;
|
|
163
|
+
if (skipped !== '0') comment += `| ⏭️ Skipped | ${skipped} |\n`;
|
|
164
|
+
comment += `| **Total** | **${total}** |\n\n`;
|
|
165
|
+
comment += `**Pass Rate:** ${passRate}%\n\n`;
|
|
166
|
+
} else {
|
|
167
|
+
comment += '_No test results found_\n\n';
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// AI QA results
|
|
171
|
+
const previewUrl = '${{ inputs.preview_url }}';
|
|
172
|
+
if (previewUrl) {
|
|
173
|
+
comment += '### 🤖 AI QA Analysis\n\n';
|
|
174
|
+
|
|
175
|
+
const aiReportPath = '_qaie/qa-report.md';
|
|
176
|
+
if (fs.existsSync(aiReportPath)) {
|
|
177
|
+
let aiReport = fs.readFileSync(aiReportPath, 'utf8');
|
|
178
|
+
// Remove the header since we have our own
|
|
179
|
+
aiReport = aiReport.replace(/^# QA Report\n+/, '');
|
|
180
|
+
comment += aiReport + '\n\n';
|
|
181
|
+
} else {
|
|
182
|
+
comment += '_AI QA analysis did not complete_\n\n';
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Artifacts link
|
|
187
|
+
comment += '---\n';
|
|
188
|
+
comment += `📊 **[View Full Playwright Report](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})**\n\n`;
|
|
189
|
+
comment += '_Download the `playwright-report` artifact and run `npx playwright show-report` to view interactive results_\n';
|
|
190
|
+
|
|
191
|
+
// Find or update existing comment
|
|
192
|
+
const { data: comments } = await github.rest.issues.listComments({
|
|
193
|
+
owner: context.repo.owner,
|
|
194
|
+
repo: context.repo.repo,
|
|
195
|
+
issue_number: context.issue.number,
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
const botComment = comments.find(c =>
|
|
199
|
+
c.user.type === 'Bot' &&
|
|
200
|
+
c.body.includes('Playwright + AI QA Report')
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
if (botComment) {
|
|
204
|
+
await github.rest.issues.updateComment({
|
|
205
|
+
owner: context.repo.owner,
|
|
206
|
+
repo: context.repo.repo,
|
|
207
|
+
comment_id: botComment.id,
|
|
208
|
+
body: comment
|
|
209
|
+
});
|
|
210
|
+
} else {
|
|
211
|
+
await github.rest.issues.createComment({
|
|
212
|
+
owner: context.repo.owner,
|
|
213
|
+
repo: context.repo.repo,
|
|
214
|
+
issue_number: context.issue.number,
|
|
215
|
+
body: comment
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
- name: Fail if tests failed
|
|
220
|
+
if: steps.playwright.outputs.failed != '0' && steps.playwright.outputs.failed != ''
|
|
221
|
+
run: |
|
|
222
|
+
echo "❌ ${{ steps.playwright.outputs.failed }} test(s) failed"
|
|
223
|
+
exit 1
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
name: qai
|
|
2
|
+
|
|
3
|
+
# This workflow is designed to be copied to your repository.
|
|
4
|
+
# It can run manually OR on pull requests.
|
|
5
|
+
#
|
|
6
|
+
# For PR testing, you need a preview deployment. Options:
|
|
7
|
+
# 1. Vercel/Netlify: Set PREVIEW_URL env var in your deployment workflow
|
|
8
|
+
# 2. workflow_call: Call this workflow from another workflow with the URL
|
|
9
|
+
# 3. Manual: Uncomment pull_request trigger and set PREVIEW_URL secret/var
|
|
10
|
+
|
|
11
|
+
on:
|
|
12
|
+
pull_request:
|
|
13
|
+
types: [opened, synchronize, reopened]
|
|
14
|
+
workflow_dispatch:
|
|
15
|
+
inputs:
|
|
16
|
+
url:
|
|
17
|
+
description: 'URL to test (required for manual runs)'
|
|
18
|
+
required: false
|
|
19
|
+
type: string
|
|
20
|
+
focus:
|
|
21
|
+
description: 'Focus area: accessibility, performance, forms, mobile, all'
|
|
22
|
+
required: false
|
|
23
|
+
type: string
|
|
24
|
+
default: 'all'
|
|
25
|
+
fail_on_bugs:
|
|
26
|
+
description: 'Fail workflow if critical/high bugs found'
|
|
27
|
+
required: false
|
|
28
|
+
type: boolean
|
|
29
|
+
default: true
|
|
30
|
+
workflow_call:
|
|
31
|
+
inputs:
|
|
32
|
+
url:
|
|
33
|
+
description: 'URL to test'
|
|
34
|
+
required: true
|
|
35
|
+
type: string
|
|
36
|
+
focus:
|
|
37
|
+
description: 'Focus area'
|
|
38
|
+
required: false
|
|
39
|
+
type: string
|
|
40
|
+
default: 'all'
|
|
41
|
+
fail_on_bugs:
|
|
42
|
+
description: 'Fail on critical/high bugs'
|
|
43
|
+
required: false
|
|
44
|
+
type: boolean
|
|
45
|
+
default: true
|
|
46
|
+
secrets:
|
|
47
|
+
ANTHROPIC_API_KEY:
|
|
48
|
+
required: true
|
|
49
|
+
|
|
50
|
+
permissions:
|
|
51
|
+
contents: read
|
|
52
|
+
pull-requests: write
|
|
53
|
+
issues: write
|
|
54
|
+
|
|
55
|
+
env:
|
|
56
|
+
NODE_VERSION: '20'
|
|
57
|
+
PLAYWRIGHT_VERSION: '1.48.0'
|
|
58
|
+
|
|
59
|
+
jobs:
|
|
60
|
+
qa-test:
|
|
61
|
+
runs-on: ubuntu-latest
|
|
62
|
+
timeout-minutes: 12
|
|
63
|
+
|
|
64
|
+
steps:
|
|
65
|
+
- name: Determine Test URL
|
|
66
|
+
id: get-url
|
|
67
|
+
env:
|
|
68
|
+
INPUT_URL: ${{ inputs.url }}
|
|
69
|
+
PREVIEW_URL: ${{ vars.PREVIEW_URL || secrets.PREVIEW_URL || '' }}
|
|
70
|
+
run: |
|
|
71
|
+
# Priority: explicit input > PREVIEW_URL var/secret
|
|
72
|
+
if [ -n "$INPUT_URL" ]; then
|
|
73
|
+
echo "test_url=$INPUT_URL" >> $GITHUB_OUTPUT
|
|
74
|
+
echo "✓ Using provided URL: $INPUT_URL"
|
|
75
|
+
elif [ -n "$PREVIEW_URL" ]; then
|
|
76
|
+
echo "test_url=$PREVIEW_URL" >> $GITHUB_OUTPUT
|
|
77
|
+
echo "✓ Using PREVIEW_URL: $PREVIEW_URL"
|
|
78
|
+
else
|
|
79
|
+
echo "❌ Error: No URL provided"
|
|
80
|
+
echo ""
|
|
81
|
+
echo "For manual runs: provide a URL in the workflow inputs"
|
|
82
|
+
echo "For PR runs: set PREVIEW_URL as a repository variable or secret"
|
|
83
|
+
echo "For Vercel/Netlify: chain this workflow after your deploy workflow"
|
|
84
|
+
exit 1
|
|
85
|
+
fi
|
|
86
|
+
|
|
87
|
+
- name: Validate API Key
|
|
88
|
+
env:
|
|
89
|
+
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
90
|
+
run: |
|
|
91
|
+
if [ -z "$ANTHROPIC_API_KEY" ]; then
|
|
92
|
+
echo "❌ Error: ANTHROPIC_API_KEY secret is not configured"
|
|
93
|
+
echo "Please add your Anthropic API key to repository secrets."
|
|
94
|
+
exit 1
|
|
95
|
+
fi
|
|
96
|
+
echo "✓ API key configured"
|
|
97
|
+
|
|
98
|
+
- name: Checkout repository
|
|
99
|
+
uses: actions/checkout@v4
|
|
100
|
+
|
|
101
|
+
- name: Setup Node.js
|
|
102
|
+
uses: actions/setup-node@v4
|
|
103
|
+
with:
|
|
104
|
+
node-version: ${{ env.NODE_VERSION }}
|
|
105
|
+
cache: 'npm'
|
|
106
|
+
|
|
107
|
+
- name: Cache Playwright Browsers
|
|
108
|
+
id: playwright-cache
|
|
109
|
+
uses: actions/cache@v4
|
|
110
|
+
with:
|
|
111
|
+
path: ~/.cache/ms-playwright
|
|
112
|
+
key: playwright-${{ runner.os }}-${{ env.PLAYWRIGHT_VERSION }}
|
|
113
|
+
restore-keys: |
|
|
114
|
+
playwright-${{ runner.os }}-
|
|
115
|
+
|
|
116
|
+
- name: Install Dependencies
|
|
117
|
+
run: npm ci
|
|
118
|
+
env:
|
|
119
|
+
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 'true'
|
|
120
|
+
|
|
121
|
+
- name: Install Playwright Browsers
|
|
122
|
+
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
|
123
|
+
run: npx playwright install chromium --with-deps
|
|
124
|
+
|
|
125
|
+
- name: Install Playwright Deps (cached)
|
|
126
|
+
if: steps.playwright-cache.outputs.cache-hit == 'true'
|
|
127
|
+
run: npx playwright install-deps chromium
|
|
128
|
+
|
|
129
|
+
- name: Check URL is reachable
|
|
130
|
+
id: url-check
|
|
131
|
+
run: |
|
|
132
|
+
echo "Checking if URL is reachable..."
|
|
133
|
+
TEST_URL="${{ steps.get-url.outputs.test_url }}"
|
|
134
|
+
|
|
135
|
+
# Try to reach the URL
|
|
136
|
+
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 "$TEST_URL" || echo "000")
|
|
137
|
+
|
|
138
|
+
if [ "$HTTP_CODE" = "000" ]; then
|
|
139
|
+
echo "⚠️ Warning: Could not reach $TEST_URL (connection failed)"
|
|
140
|
+
echo "reachable=false" >> $GITHUB_OUTPUT
|
|
141
|
+
elif [ "$HTTP_CODE" -ge 400 ]; then
|
|
142
|
+
echo "⚠️ Warning: $TEST_URL returned HTTP $HTTP_CODE"
|
|
143
|
+
echo "reachable=false" >> $GITHUB_OUTPUT
|
|
144
|
+
else
|
|
145
|
+
echo "✓ URL is reachable (HTTP $HTTP_CODE)"
|
|
146
|
+
echo "reachable=true" >> $GITHUB_OUTPUT
|
|
147
|
+
fi
|
|
148
|
+
|
|
149
|
+
- name: Create Screenshots Directory
|
|
150
|
+
run: mkdir -p screenshots
|
|
151
|
+
|
|
152
|
+
- name: Run qai
|
|
153
|
+
id: qa-run
|
|
154
|
+
env:
|
|
155
|
+
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
156
|
+
TEST_URL: ${{ steps.get-url.outputs.test_url }}
|
|
157
|
+
TEST_FOCUS: ${{ inputs.focus || 'all' }}
|
|
158
|
+
run: |
|
|
159
|
+
echo "::group::QA Testing Configuration"
|
|
160
|
+
echo "URL: $TEST_URL"
|
|
161
|
+
echo "Focus Area: $TEST_FOCUS"
|
|
162
|
+
echo "::endgroup::"
|
|
163
|
+
|
|
164
|
+
echo "::group::Starting qai"
|
|
165
|
+
START_TIME=$(date +%s)
|
|
166
|
+
|
|
167
|
+
# 5 minute timeout, max 15 turns - be aggressive about speed
|
|
168
|
+
timeout 300 npx @anthropic-ai/claude-code --print \
|
|
169
|
+
--mcp-config .claude/mcp-config.json \
|
|
170
|
+
--max-turns 15 \
|
|
171
|
+
"SPEED IS CRITICAL. You have 3 minutes max.
|
|
172
|
+
|
|
173
|
+
Test $TEST_URL:
|
|
174
|
+
1. Navigate to URL
|
|
175
|
+
2. Screenshot desktop view -> ./screenshots/desktop.png
|
|
176
|
+
3. Screenshot mobile (375px) -> ./screenshots/mobile.png
|
|
177
|
+
4. Check for console errors
|
|
178
|
+
5. Click 2-3 buttons/links to verify they work
|
|
179
|
+
6. Write ./qa-report.md: list any bugs found, or 'No issues found'
|
|
180
|
+
|
|
181
|
+
DO NOT: test multiple viewports, do exhaustive testing, write long reports.
|
|
182
|
+
STOP after basic checks. Speed > thoroughness." || {
|
|
183
|
+
EXIT_CODE=$?
|
|
184
|
+
if [ $EXIT_CODE -eq 124 ]; then
|
|
185
|
+
echo "⚠️ QA testing timed out after 5 minutes"
|
|
186
|
+
else
|
|
187
|
+
echo "⚠️ QA testing encountered an error (exit code: $EXIT_CODE)"
|
|
188
|
+
fi
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
END_TIME=$(date +%s)
|
|
192
|
+
DURATION=$((END_TIME - START_TIME))
|
|
193
|
+
echo "✓ QA testing completed in ${DURATION}s"
|
|
194
|
+
echo "::endgroup::"
|
|
195
|
+
echo "qa_duration_seconds=$DURATION" >> $GITHUB_OUTPUT
|
|
196
|
+
|
|
197
|
+
- name: Upload Screenshots
|
|
198
|
+
uses: actions/upload-artifact@v4
|
|
199
|
+
if: always()
|
|
200
|
+
with:
|
|
201
|
+
name: qa-screenshots-${{ github.run_number }}
|
|
202
|
+
path: screenshots/
|
|
203
|
+
retention-days: 14
|
|
204
|
+
if-no-files-found: ignore
|
|
205
|
+
|
|
206
|
+
- name: Upload QA Report
|
|
207
|
+
uses: actions/upload-artifact@v4
|
|
208
|
+
if: always()
|
|
209
|
+
with:
|
|
210
|
+
name: qa-report-${{ github.run_number }}
|
|
211
|
+
path: qa-report.md
|
|
212
|
+
retention-days: 14
|
|
213
|
+
if-no-files-found: ignore
|
|
214
|
+
|
|
215
|
+
- name: Summary
|
|
216
|
+
if: always()
|
|
217
|
+
run: |
|
|
218
|
+
echo "## qai Results" >> $GITHUB_STEP_SUMMARY
|
|
219
|
+
echo "" >> $GITHUB_STEP_SUMMARY
|
|
220
|
+
echo "**URL Tested:** ${{ steps.get-url.outputs.test_url }}" >> $GITHUB_STEP_SUMMARY
|
|
221
|
+
echo "**Focus Area:** ${{ inputs.focus || 'all' }}" >> $GITHUB_STEP_SUMMARY
|
|
222
|
+
echo "" >> $GITHUB_STEP_SUMMARY
|
|
223
|
+
|
|
224
|
+
if [ -f "qa-report.md" ]; then
|
|
225
|
+
echo "### Report" >> $GITHUB_STEP_SUMMARY
|
|
226
|
+
cat qa-report.md >> $GITHUB_STEP_SUMMARY
|
|
227
|
+
else
|
|
228
|
+
echo "⚠️ No QA report generated. Check the logs for details." >> $GITHUB_STEP_SUMMARY
|
|
229
|
+
fi
|
|
230
|
+
|
|
231
|
+
- name: Comment on PR
|
|
232
|
+
if: github.event_name == 'pull_request' && always()
|
|
233
|
+
uses: actions/github-script@v7
|
|
234
|
+
with:
|
|
235
|
+
script: |
|
|
236
|
+
const fs = require('fs');
|
|
237
|
+
|
|
238
|
+
let body = '## 🤖 qai Report\n\n';
|
|
239
|
+
body += `**URL Tested:** ${{ steps.get-url.outputs.test_url }}\n`;
|
|
240
|
+
body += `**Focus Area:** ${{ inputs.focus || 'all' }}\n\n`;
|
|
241
|
+
|
|
242
|
+
if (fs.existsSync('qa-report.md')) {
|
|
243
|
+
const report = fs.readFileSync('qa-report.md', 'utf8');
|
|
244
|
+
body += report;
|
|
245
|
+
} else {
|
|
246
|
+
body += '⚠️ No QA report generated. Check the [workflow logs](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) for details.\n';
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
body += `\n\n### 📎 Artifacts\n`;
|
|
250
|
+
body += `- [Screenshots](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})\n`;
|
|
251
|
+
body += `- [Full Report](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})\n`;
|
|
252
|
+
body += '\n---\n*Automated QA testing by [qai](https://github.com/tyler-james-bridges/qaie)*';
|
|
253
|
+
|
|
254
|
+
// Find existing comment to update (avoid spam)
|
|
255
|
+
const { data: comments } = await github.rest.issues.listComments({
|
|
256
|
+
owner: context.repo.owner,
|
|
257
|
+
repo: context.repo.repo,
|
|
258
|
+
issue_number: context.issue.number,
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
const botComment = comments.find(comment =>
|
|
262
|
+
comment.user.type === 'Bot' &&
|
|
263
|
+
comment.body.includes('qai Report')
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
if (botComment) {
|
|
267
|
+
await github.rest.issues.updateComment({
|
|
268
|
+
owner: context.repo.owner,
|
|
269
|
+
repo: context.repo.repo,
|
|
270
|
+
comment_id: botComment.id,
|
|
271
|
+
body: body
|
|
272
|
+
});
|
|
273
|
+
} else {
|
|
274
|
+
await github.rest.issues.createComment({
|
|
275
|
+
owner: context.repo.owner,
|
|
276
|
+
repo: context.repo.repo,
|
|
277
|
+
issue_number: context.issue.number,
|
|
278
|
+
body: body
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
- name: Check for critical bugs
|
|
283
|
+
if: ${{ inputs.fail_on_bugs }}
|
|
284
|
+
run: |
|
|
285
|
+
if [ ! -f "qa-report.md" ]; then
|
|
286
|
+
echo "No QA report found - skipping bug check"
|
|
287
|
+
exit 0
|
|
288
|
+
fi
|
|
289
|
+
|
|
290
|
+
echo "Checking for critical/high severity bugs..."
|
|
291
|
+
|
|
292
|
+
# Case-insensitive search for critical or high severity indicators
|
|
293
|
+
CRITICAL_COUNT=$(grep -i -c -E '(critical|severity:\s*critical)' qa-report.md || echo "0")
|
|
294
|
+
HIGH_COUNT=$(grep -i -c -E '(high|severity:\s*high)' qa-report.md || echo "0")
|
|
295
|
+
|
|
296
|
+
echo "Critical bugs: $CRITICAL_COUNT"
|
|
297
|
+
echo "High severity bugs: $HIGH_COUNT"
|
|
298
|
+
|
|
299
|
+
if [ "$CRITICAL_COUNT" -gt 0 ]; then
|
|
300
|
+
echo "::error::Found $CRITICAL_COUNT critical bug(s) - failing workflow"
|
|
301
|
+
exit 1
|
|
302
|
+
fi
|
|
303
|
+
|
|
304
|
+
if [ "$HIGH_COUNT" -gt 0 ]; then
|
|
305
|
+
echo "::warning::Found $HIGH_COUNT high severity bug(s) - failing workflow"
|
|
306
|
+
exit 1
|
|
307
|
+
fi
|
|
308
|
+
|
|
309
|
+
echo "✓ No critical/high severity bugs found"
|