Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 52 additions & 58 deletions .github/actions/auth/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,102 +1,96 @@
import type { AuthContextOutput } from "./types.d.js";
import crypto from "node:crypto";
import process from "node:process";
import * as url from "node:url";
import core from "@actions/core";
import playwright from "playwright";
import type {AuthContextOutput} from './types.d.js'
import process from 'node:process'
import core from '@actions/core'
import playwright from 'playwright'

export default async function () {
core.info("Starting 'auth' action");
core.info("Starting 'auth' action")

let browser: playwright.Browser | undefined;
let context: playwright.BrowserContext | undefined;
let page: playwright.Page | undefined;
let browser: playwright.Browser | undefined
let context: playwright.BrowserContext | undefined
let page: playwright.Page | undefined
try {
// Get inputs
const loginUrl = core.getInput("login_url", { required: true });
const username = core.getInput("username", { required: true });
const password = core.getInput("password", { required: true });
core.setSecret(password);

// Determine storage path for authenticated session state
// Playwright will create missing directories, if needed
const actionDirectory = `${url.fileURLToPath(new URL(import.meta.url))}/..`;
const sessionStatePath = `${
process.env.RUNNER_TEMP ?? actionDirectory
}/.auth/${crypto.randomUUID()}/sessionState.json`;
const loginUrl = core.getInput('login_url', {required: true})
const username = core.getInput('username', {required: true})
const password = core.getInput('password', {required: true})
core.setSecret(password)

// Launch a headless browser
browser = await playwright.chromium.launch({
headless: true,
executablePath: process.env.CI ? "/usr/bin/google-chrome" : undefined,
});
executablePath: process.env.CI ? '/usr/bin/google-chrome' : undefined,
})
context = await browser.newContext({
// Try HTTP Basic authentication
httpCredentials: {
username,
password,
},
});
page = await context.newPage();
})
page = await context.newPage()

// Navigate to login page
core.info("Navigating to login page");
await page.goto(loginUrl);
core.info('Navigating to login page')
await page.goto(loginUrl)

// Check for a login form.
// If no login form is found, then either HTTP Basic auth succeeded, or the page does not require authentication.
core.info("Checking for login form");
core.info('Checking for login form')
const [usernameField, passwordField] = await Promise.all([
page.getByLabel(/user ?name/i).first(),
page.getByLabel(/password/i).first(),
]);
const [usernameFieldExists, passwordFieldExists] = await Promise.all([
usernameField.count(),
passwordField.count(),
]);
])
const [usernameFieldExists, passwordFieldExists] = await Promise.all([usernameField.count(), passwordField.count()])
if (usernameFieldExists && passwordFieldExists) {
// Try form authentication
core.info("Filling username");
await usernameField.fill(username);
core.info("Filling password");
await passwordField.fill(password);
core.info("Logging in");
core.info('Filling username')
await usernameField.fill(username)
core.info('Filling password')
await passwordField.fill(password)
core.info('Logging in')
await page
.getByLabel(/password/i)
.locator("xpath=ancestor::form")
.evaluate((form) => (form as HTMLFormElement).submit());
.locator('xpath=ancestor::form')
.evaluate(form => (form as HTMLFormElement).submit())
} else {
core.info("No login form detected");
core.info('No login form detected')
// This occurs if HTTP Basic auth succeeded, or if the page does not require authentication.
}

// Output authenticated session state
const { cookies, origins } = await context.storageState();
const {cookies, origins} = await context.storageState()
const authContextOutput: AuthContextOutput = {
username,
password,
cookies,
localStorage: origins.reduce((acc, { origin, localStorage }) => {
acc[origin] = localStorage.reduce((acc, { name, value }) => {
acc[name] = value;
return acc;
}, {} as Record<string, string>);
return acc;
}, {} as Record<string, Record<string, string>>),
};
core.setOutput("auth_context", JSON.stringify(authContextOutput));
core.debug("Output: 'auth_context'");
localStorage: origins.reduce(
(acc, {origin, localStorage}) => {
acc[origin] = localStorage.reduce(
(acc, {name, value}) => {
acc[name] = value
return acc
},
{} as Record<string, string>,
)
return acc
},
{} as Record<string, Record<string, string>>,
),
}
core.setOutput('auth_context', JSON.stringify(authContextOutput))
core.debug("Output: 'auth_context'")
} catch (error) {
if (page) {
core.info(`Errored at page URL: ${page.url()}`);
core.info(`Errored at page URL: ${page.url()}`)
}
core.setFailed(`${error}`);
process.exit(1);
core.setFailed(`${error}`)
process.exit(1)
} finally {
// Clean up
await context?.close();
await browser?.close();
await context?.close()
await browser?.close()
}

core.info("Finished 'auth' action");
core.info("Finished 'auth' action")
}
34 changes: 17 additions & 17 deletions .github/actions/auth/src/types.d.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
export type Cookie = {
name: string;
value: string;
domain: string;
path: string;
expires?: number;
httpOnly?: boolean;
secure?: boolean;
sameSite?: "Strict" | "Lax" | "None";
};
name: string
value: string
domain: string
path: string
expires?: number
httpOnly?: boolean
secure?: boolean
sameSite?: 'Strict' | 'Lax' | 'None'
}

export type LocalStorage = {
[origin: string]: {
[key: string]: string;
};
};
[key: string]: string
}
}

export type AuthContextOutput = {
username?: string;
password?: string;
cookies?: Cookie[];
localStorage?: LocalStorage;
};
username?: string
password?: string
cookies?: Cookie[]
localStorage?: LocalStorage
}
62 changes: 30 additions & 32 deletions .github/actions/file/src/Issue.ts
Original file line number Diff line number Diff line change
@@ -1,44 +1,44 @@
import type { Issue as IssueInput } from "./types.d.js";
import type {Issue as IssueInput} from './types.d.js'

export class Issue implements IssueInput {
#url!: string;
#url!: string
#parsedUrl!: {
owner: string;
repository: string;
issueNumber: number;
};
nodeId: string;
id: number;
title: string;
state?: "open" | "reopened" | "closed";

constructor({ url, nodeId, id, title, state }: IssueInput) {
this.url = url;
this.nodeId = nodeId;
this.id = id;
this.title = title;
this.state = state;
owner: string
repository: string
issueNumber: number
}
nodeId: string
id: number
title: string
state?: 'open' | 'reopened' | 'closed'

constructor({url, nodeId, id, title, state}: IssueInput) {
this.url = url
this.nodeId = nodeId
this.id = id
this.title = title
this.state = state
}

set url(newUrl: string) {
this.#url = newUrl;
this.#parsedUrl = this.#parseUrl();
this.#url = newUrl
this.#parsedUrl = this.#parseUrl()
}

get url(): string {
return this.#url;
return this.#url
}

get owner(): string {
return this.#parsedUrl.owner;
return this.#parsedUrl.owner
}

get repository(): string {
return this.#parsedUrl.repository;
return this.#parsedUrl.repository
}

get issueNumber(): number {
return this.#parsedUrl.issueNumber;
return this.#parsedUrl.issueNumber
}

/**
Expand All @@ -47,17 +47,15 @@ export class Issue implements IssueInput {
* @throws The provided URL is unparseable due to its unexpected format.
*/
#parseUrl(): {
owner: string;
repository: string;
issueNumber: number;
owner: string
repository: string
issueNumber: number
} {
const { owner, repository, issueNumber } =
/\/(?<owner>[^/]+)\/(?<repository>[^/]+)\/issues\/(?<issueNumber>\d+)(?:[/?#]|$)/.exec(
this.#url
)?.groups || {};
const {owner, repository, issueNumber} =
/\/(?<owner>[^/]+)\/(?<repository>[^/]+)\/issues\/(?<issueNumber>\d+)(?:[/?#]|$)/.exec(this.#url)?.groups || {}
if (!owner || !repository || !issueNumber) {
throw new Error(`Could not parse issue URL: ${this.#url}`);
throw new Error(`Could not parse issue URL: ${this.#url}`)
}
return { owner, repository, issueNumber: Number(issueNumber) };
return {owner, repository, issueNumber: Number(issueNumber)}
}
}
12 changes: 6 additions & 6 deletions .github/actions/file/src/closeIssue.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import type { Octokit } from '@octokit/core';
import { Issue } from './Issue.js';
import type {Octokit} from '@octokit/core'
import {Issue} from './Issue.js'

export async function closeIssue(octokit: Octokit, { owner, repository, issueNumber }: Issue) {
export async function closeIssue(octokit: Octokit, {owner, repository, issueNumber}: Issue) {
return octokit.request(`PATCH /repos/${owner}/${repository}/issues/${issueNumber}`, {
owner,
repository,
issue_number: issueNumber,
state: 'closed'
});
}
state: 'closed',
})
}
31 changes: 14 additions & 17 deletions .github/actions/file/src/generateIssueBody.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,28 @@
import type { Finding } from "./types.d.js";
import type {Finding} from './types.d.js'

export function generateIssueBody(finding: Finding, repoWithOwner: string): string {
export function generateIssueBody(finding: Finding): string {
const solutionLong = finding.solutionLong
?.split("\n")
.map((line: string) =>
!line.trim().startsWith("Fix any") &&
!line.trim().startsWith("Fix all") &&
line.trim() !== ""
? `- ${line}`
: line
)
.join("\n");
const acceptanceCriteria = `## Acceptance Criteria
?.split('\n')
.map((line: string) =>
!line.trim().startsWith('Fix any') && !line.trim().startsWith('Fix all') && line.trim() !== ''
? `- ${line}`
: line,
)
.join('\n')
const acceptanceCriteria = `## Acceptance Criteria
- [ ] The specific axe violation reported in this issue is no longer reproducible.
- [ ] The fix MUST meet WCAG 2.1 guidelines OR the accessibility standards specified by the repository or organization.
- [ ] A test SHOULD be added to ensure this specific axe violation does not regress.
- [ ] This PR MUST NOT introduce any new accessibility issues or regressions.
`;
const body = `## What
`
const body = `## What
An accessibility scan flagged the element \`${finding.html}\` on ${finding.url} because ${finding.problemShort}. Learn more about why this was flagged by visiting ${finding.problemUrl}.

To fix this, ${finding.solutionShort}.
${solutionLong ? `\nSpecifically:\n\n${solutionLong}` : ''}

${acceptanceCriteria}
`;
`

return body;
return body
}

Loading