Skip to content

Commit 71b126d

Browse files
committed
feat: Add 'Auth' action
1 parent 86e6d3f commit 71b126d

File tree

7 files changed

+370
-0
lines changed

7 files changed

+370
-0
lines changed

.github/actions/auth/README.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# auth
2+
3+
Log in using Playwright, then write authenticated session state to a file for reuse in other Playwright sessions.
4+
5+
## Usage
6+
7+
### Inputs
8+
9+
#### `login_url`
10+
11+
**Required** Login page URL. For example, `https://github.com/login`
12+
13+
#### `username`
14+
15+
**Required** Username.
16+
17+
#### `password`
18+
19+
**Required** Password.
20+
21+
### Outputs
22+
23+
#### `session_state_path`
24+
25+
Path to a file containing authenticated session state.

.github/actions/auth/action.yml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
name: "Auth"
2+
description: "Log in using Playwright, then write authenticated session state to a file for reuse in other Playwright sessions."
3+
4+
inputs:
5+
login_url:
6+
description: "Login page URL"
7+
required: true
8+
username:
9+
description: "Username"
10+
required: true
11+
password:
12+
description: "Password"
13+
required: true
14+
15+
outputs:
16+
session_state_path:
17+
description: "Path to a file containing authenticated session state"
18+
19+
runs:
20+
using: "node20"
21+
main: "bootstrap.js"
22+
23+
branding:
24+
icon: "lock"
25+
color: "blue"

.github/actions/auth/bootstrap.js

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
#!/usr/bin/env node
2+
//@ts-check
3+
4+
import fs from 'node:fs'
5+
import * as url from 'node:url'
6+
import { spawn } from 'node:child_process'
7+
8+
function spawnPromisified(command, args, { quiet = false, ...options } = {}) {
9+
return new Promise((resolve, reject) => {
10+
const proc = spawn(command, args, options)
11+
proc.stdout.setEncoding('utf8')
12+
proc.stdout.on('data', (data) => {
13+
if (!quiet) {
14+
console.log(data)
15+
}
16+
})
17+
proc.stderr.setEncoding('utf8')
18+
proc.stderr.on('data', (data) => {
19+
console.error(data)
20+
})
21+
proc.on('close', (code) => {
22+
if (code !== 0) {
23+
reject(code)
24+
} else {
25+
resolve(code)
26+
}
27+
})
28+
})
29+
}
30+
31+
await (async () => {
32+
// If dependencies are not vendored-in, install them at runtime.
33+
try {
34+
await fs.accessSync(
35+
url.fileURLToPath(new URL('./node_modules', import.meta.url)),
36+
fs.constants.R_OK
37+
)
38+
} catch {
39+
try {
40+
await spawnPromisified('npm', ['ci'], {
41+
cwd: url.fileURLToPath(new URL('.', import.meta.url)),
42+
quiet: true
43+
})
44+
} catch {
45+
process.exit(1)
46+
}
47+
} finally {
48+
// Compile TypeScript.
49+
try {
50+
await spawnPromisified('npm', ['run', 'build'], {
51+
cwd: url.fileURLToPath(new URL('.', import.meta.url)),
52+
quiet: true
53+
})
54+
} catch {
55+
process.exit(1)
56+
}
57+
// Run the main script.
58+
const action = await import('./dist/index.js')
59+
await action.default()
60+
}
61+
})()

.github/actions/auth/package-lock.json

Lines changed: 161 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.github/actions/auth/package.json

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"name": "auth",
3+
"version": "1.0.0",
4+
"description": "Log in using Playwright, then write authenticated session state to a file for reuse in other Playwright sessions.",
5+
"main": "dist/index.js",
6+
"module": "dist/index.js",
7+
"scripts": {
8+
"start": "node bootstrap.js",
9+
"build": "tsc"
10+
},
11+
"keywords": [],
12+
"author": "GitHub",
13+
"license": "MIT",
14+
"type": "module",
15+
"dependencies": {
16+
"@actions/core": "^1.11.1",
17+
"playwright": "^1.55.1"
18+
},
19+
"devDependencies": {
20+
"@types/node": "^24.5.2",
21+
"typescript": "^5.8.3"
22+
}
23+
}

.github/actions/auth/src/index.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import crypto from 'node:crypto';
2+
import fs from 'node:fs/promises';
3+
import process from 'node:process';
4+
import * as url from 'node:url';
5+
import core from "@actions/core";
6+
import playwright from 'playwright';
7+
8+
export default async function () {
9+
core.info("Starting 'auth' action");
10+
11+
let browser: playwright.Browser | undefined;
12+
let context: playwright.BrowserContext | undefined;
13+
let page: playwright.Page | undefined;
14+
try {
15+
// Get inputs
16+
const loginUrl = core.getInput('login_url', { required: true });
17+
const username = core.getInput('username', { required: true });
18+
const password = core.getInput('password', { required: true });
19+
core.setSecret(password);
20+
21+
// Create a temporary directory for authenticated session state
22+
const actionDirectory = `${url.fileURLToPath(new URL(import.meta.url))}/..`;
23+
const sessionStateDirectory = `${process.env.RUNNER_TEMP ?? actionDirectory}/.auth/${crypto.randomUUID()}`;
24+
await fs.mkdir(sessionStateDirectory, { recursive: true });
25+
const sessionStatePath = `${sessionStateDirectory}/sessionState.json`;
26+
27+
// Launch a headless browser
28+
browser = await playwright.chromium.launch({ headless: true, executablePath: process.env.CI ? '/usr/bin/google-chrome' : undefined });
29+
context = await browser.newContext();
30+
page = await context.newPage();
31+
32+
// Log in
33+
core.info("Navigating to login page");
34+
await page.goto(loginUrl);
35+
core.info('Filling username');
36+
await page.getByLabel(/username/i).fill(username);
37+
core.info('Filling password');
38+
await page.getByLabel(/password/i).fill(password);
39+
core.info('Logging in');
40+
await page.getByLabel(/password/i)
41+
.locator('xpath=ancestor::form')
42+
.evaluate((form) => (form as HTMLFormElement).submit());
43+
44+
// Write authenticated session state to a file and output its path
45+
await context.storageState({ path: sessionStatePath });
46+
core.setOutput("session_state_path", sessionStatePath);
47+
core.info(`Wrote authenticated session state to ${sessionStatePath}`);
48+
} catch (error) {
49+
if (page) {
50+
core.info(`Errored at page URL: ${page.url()}`);
51+
}
52+
core.setFailed(`${error}`);
53+
process.exit(1);
54+
} finally {
55+
// Clean up
56+
await context?.close();
57+
await browser?.close();
58+
}
59+
60+
core.info("Finished 'auth' action");
61+
}

.github/actions/auth/tsconfig.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"compilerOptions": {
3+
"target": "esnext",
4+
"module": "nodenext",
5+
"moduleResolution": "nodenext",
6+
"esModuleInterop": true,
7+
"strict": true,
8+
"rootDir": "src",
9+
"outDir": "dist"
10+
},
11+
"include": [
12+
"src/**/*.ts"
13+
]
14+
}

0 commit comments

Comments
 (0)