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.
@@ -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"