-
Notifications
You must be signed in to change notification settings - Fork 20
feat: add accessibility scanner workflow #1636
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
a9814d7
428aff1
e683c06
447d45e
7f4b76c
a63906b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,226 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| name: Accessibility Scanner | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| on: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| workflow_dispatch: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # TODO: remove push trigger before merging — workflow_dispatch only works from the default branch | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| push: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| branches: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - a11y-scanner-setup | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
onchul marked this conversation as resolved.
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| jobs: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| build: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| build: | |
| build: | |
| permissions: | |
| contents: read | |
| packages: write |
Check warning
Code scanning / CodeQL
Workflow does not contain permissions Medium
Copilot
AI
Apr 15, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Postgres container password is hardcoded (POSTGRES_PASSWORD=secret) and the same value is embedded later in DB_URL. Please wire the DB password through a secret or a generated runtime value (and reference it consistently) so credentials aren’t committed to the workflow and can be rotated safely.
Copilot
AI
Apr 16, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The auth cookie extraction drops any lines starting with # (awk '!/^#/'). curl writes HttpOnly cookies in the Netscape cookie jar as lines prefixed with #HttpOnly_..., so this will likely discard the session cookie entirely and the scanner will run unauthenticated. Adjust the parsing to retain #HttpOnly_ cookie lines (stripping the prefix) and derive httpOnly correctly (the 2nd Netscape field is the include-subdomains flag, not HttpOnly).
Copilot
AI
Apr 16, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
auth_context is built to include username and password and then written to a step output. Even if the password is not a GitHub secret here, embedding credentials in outputs increases the chance of accidental exposure (logs/action debug output) and isn't necessary if cookie-based auth is sufficient. Consider omitting credentials from auth_context and only passing the minimal cookie/storage state required for Playwright.
Copilot
AI
Apr 16, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The "Verify axe timing issue" step installs Playwright/Axe at runtime via npm install without pinning versions or using actions/setup-node/npm ci with a lockfile, which can make the workflow slow and non-reproducible. If this is only for debugging, remove it before merging; otherwise, align it with the repo’s E2E workflow pattern (setup-node + cached npm ci + pinned deps).
| - name: Verify axe timing issue (immediate vs after wait) | |
| run: | | |
| npm install playwright @axe-core/playwright | |
| npx playwright install chromium --with-deps | |
| node << 'SCRIPT' | |
| const { chromium } = require('playwright'); | |
| const { default: AxeBuilder } = require('@axe-core/playwright'); | |
| const AUTH_CONTEXT = JSON.parse(process.env.AUTH_CONTEXT); | |
| (async () => { | |
| const browser = await chromium.launch(); | |
| const context = await browser.newContext({ | |
| storageState: { | |
| cookies: AUTH_CONTEXT.cookies.map(c => ({ ...c, sameSite: 'Lax', expires: -1 })), | |
| origins: [] | |
| } | |
| }); | |
| const urls = [ | |
| 'http://localhost:3080/admin/theme', | |
| 'http://localhost:3080/admin/dashboard', | |
| 'http://localhost:3080/chat' | |
| ]; | |
| for (const url of urls) { | |
| const page = await context.newPage(); | |
| await page.goto(url); | |
| const immediate = await new AxeBuilder({ page }).analyze(); | |
| console.log(`\n=== ${url} ===`); | |
| console.log('Without wait:', immediate.violations.map(v => v.id)); | |
| await page.waitForLoadState('networkidle'); | |
| const waited = await new AxeBuilder({ page }).analyze(); | |
| console.log('With wait: ', waited.violations.map(v => v.id)); | |
| await page.close(); | |
| } | |
| await browser.close(); | |
| })(); | |
| SCRIPT | |
| env: | |
| AUTH_CONTEXT: ${{ steps.auth.outputs.auth_context }} |
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -122,11 +122,22 @@ function LoginForm() { | |||||||||
| )} | ||||||||||
|
|
||||||||||
| <div className="mb-3"> | ||||||||||
| <TextInput placeholder={texts.common.email} autoFocus key={form.key('email')} {...form.getInputProps('email')} /> | ||||||||||
| <TextInput | ||||||||||
| placeholder={texts.common.email} | ||||||||||
| autoFocus | ||||||||||
| key={form.key('email')} | ||||||||||
| label="username" | ||||||||||
|
||||||||||
| label="username" | |
| label={texts.common.email} |
Copilot
AI
Apr 16, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The TextInput is bound to the email field (getInputProps('email') and placeholder uses texts.common.email), but the accessible label is set to the hard-coded string "username". This will be announced incorrectly by screen readers and is inconsistent with the rest of the UI that uses localized texts.* labels. Use an email-appropriate, localized label (and optionally visually-hide it if you don't want a visible label).
| label="username" | |
| label={texts.common.email} |
Copilot
AI
Apr 16, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The password field uses a hard-coded label string ("password") instead of the localized texts.common.password used across the app. This is user-visible and also affects accessibility naming/translation consistency; please switch to the shared texts label (or an equivalent localized string).
| label="password" | |
| label={texts.common.password} |
Uh oh!
There was an error while loading. Please reload this page.