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
6 changes: 3 additions & 3 deletions .github/workflows/css-selectors.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,15 @@ jobs:
NEEDRESTART_SUSPEND: 1

steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7

- name: Install pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@v6
with:
version: 10

- name: Setup node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v7
with:
node-version: lts/*
cache: "pnpm"
Expand Down
16 changes: 7 additions & 9 deletions .github/workflows/e2e.yaml
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
name: E2E Tests

# No `paths:` filter on purpose. This is a required status check, and a
# workflow that is skipped by a path filter never reports a status at all, so
# the check sits on "Expected" and the PR can never merge. Running it on every
# PR costs ~45s and keeps the gate honest.
on:
pull_request:
branches: [main, dev]
types: [opened, reopened, synchronize, ready_for_review]
paths:
- 'src/**'
- 'playwright.config.ts'
- 'package.json'
- 'pnpm-lock.yaml'
- 'wxt.config.ts'

jobs:
e2e:
Expand All @@ -24,15 +22,15 @@ jobs:
NEEDRESTART_SUSPEND: 1

steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7

- name: Install pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@v6
with:
version: 10

- name: Setup node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v7
with:
node-version: lts/*
cache: "pnpm"
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/prettier-check.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@ jobs:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7

- name: Install pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@v6
with:
version: 10

- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v7
with:
node-version: lts/*
cache: "pnpm"
Expand Down
16 changes: 8 additions & 8 deletions .github/workflows/publish.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
has_chrome: ${{ steps.chrome.outputs.has_chrome }}
version: ${{ steps.version.outputs.version }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
with:
fetch-depth: 0

Expand Down Expand Up @@ -61,15 +61,15 @@ jobs:
if: needs.check.outputs.should_publish == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7

- name: Install pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@v6
with:
version: 10

- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v7
with:
node-version: lts/*
cache: "pnpm"
Expand All @@ -96,15 +96,15 @@ jobs:
if: needs.check.outputs.should_publish == 'true' && needs.check.outputs.has_chrome == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7

- name: Install pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@v6
with:
version: 10

- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v7
with:
node-version: lts/*
cache: "pnpm"
Expand Down Expand Up @@ -143,7 +143,7 @@ jobs:
env:
VERSION: ${{ needs.check.outputs.version }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7

- name: Tag and create GitHub release
env:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/semver.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:

steps:
- name: Checkout PR branch
uses: actions/checkout@v4
uses: actions/checkout@v7

- name: Fetch main branch
run: git fetch origin main
Expand Down
16 changes: 7 additions & 9 deletions .github/workflows/unit.yaml
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
name: Unit Tests

# No `paths:` filter on purpose. This is a required status check, and a
# workflow that is skipped by a path filter never reports a status at all, so
# the check sits on "Expected" and the PR can never merge. Running it on every
# PR costs ~30s and keeps the gate honest.
on:
pull_request:
branches: [main, dev]
types: [opened, reopened, synchronize, ready_for_review]
paths:
- 'src/**'
- 'vitest.config.ts'
- 'package.json'
- 'pnpm-lock.yaml'
- 'wxt.config.ts'

jobs:
unit:
Expand All @@ -18,15 +16,15 @@ jobs:
timeout-minutes: 10

steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7

- name: Install pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@v6
with:
version: 10

- name: Setup node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v7
with:
node-version: lts/*
cache: "pnpm"
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"zip": "wxt zip",
"zip:firefox": "wxt zip -b firefox",
"check": "svelte-check --tsconfig ./tsconfig.json",
"chrome:token": "node scripts/chrome-refresh-token.ts",
"postinstall": "wxt prepare",
"lint": "pnpm build && web-ext lint --source-dir .output/firefox-mv2",
"test": "playwright test --project=e2e --reporter=list",
Expand Down
117 changes: 117 additions & 0 deletions scripts/chrome-refresh-token.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
#!/usr/bin/env node

/**
* Mints a Chrome Web Store refresh token for the publish workflow.
*
* `wxt submit init` is the intended way to do this, but it still requests the
* out-of-band redirect (`urn:ietf:wg:oauth:2.0:oob`) that Google blocked in
* 2022, so it fails with "Access blocked: invalid_request". It also omits
* `access_type=offline`, without which Google returns no refresh token at all.
*
* This script does the same job with the loopback redirect that replaced OOB.
* It can be deleted once `wxt submit init` is fixed upstream.
*
* Requires an OAuth client of type "Desktop" (loopback is rejected for other
* client types) with the Chrome Web Store API enabled.
*
* Usage:
* pnpm chrome:token
*
* Credentials are read from the environment so they never reach shell history:
* read -rsp "Client secret: " S && echo && CHROME_CLIENT_ID=<id> CHROME_CLIENT_SECRET="$S" pnpm chrome:token
*
* Store the result with:
* printf %s "<token>" | gh secret set CHROME_REFRESH_TOKEN
*/

import http from "node:http"

const clientId = process.env.CHROME_CLIENT_ID
const clientSecret = process.env.CHROME_CLIENT_SECRET
const port = Number(process.env.PORT || 8080)
const redirectUri = `http://localhost:${port}`
const scope = "https://www.googleapis.com/auth/chromewebstore"

if (!clientId || !clientSecret) {
console.error(
"Set CHROME_CLIENT_ID and CHROME_CLIENT_SECRET in the environment.",
)
console.error(
'Example: read -rsp "Client secret: " S && echo && CHROME_CLIENT_ID=<id> CHROME_CLIENT_SECRET="$S" pnpm chrome:token',
)
process.exit(1)
}

// access_type=offline is what makes Google return a refresh token rather than
// only an access token. prompt=consent forces a new one even when this client
// has been authorized before, so re-running always yields a usable token.
const authUrl =
"https://accounts.google.com/o/oauth2/v2/auth?" +
new URLSearchParams({
client_id: clientId,
redirect_uri: redirectUri,
response_type: "code",
scope,
access_type: "offline",
prompt: "consent",
})

console.log("\nOpen this URL and approve access:\n")
console.log(authUrl)
console.log(`\nWaiting for the redirect on ${redirectUri} ...\n`)

const server = http.createServer(async (req, res) => {
const url = new URL(req.url ?? "/", redirectUri)
const code = url.searchParams.get("code")
const error = url.searchParams.get("error")

if (error) {
res.end(`Authorization failed: ${error}. You can close this tab.`)
console.error(`\nAuthorization failed: ${error}`)
server.close()
process.exit(1)
}

// Browsers also request /favicon.ico, which carries no code.
if (!code) {
res.end("Waiting for the authorization code...")
return
}

res.end("Authorized. You can close this tab and return to the terminal.")

const response = await fetch("https://oauth2.googleapis.com/token", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
code,
client_id: clientId,
client_secret: clientSecret,
redirect_uri: redirectUri,
grant_type: "authorization_code",
}),
})
const body = await response.json()

if (!body.refresh_token) {
console.error("\nNo refresh token returned. Response:\n", body)
if (body.error === "invalid_client") {
console.error(
"\ninvalid_client means the secret does not belong to this client ID.",
)
}
server.close()
process.exit(1)
}

console.log("\nRefresh token:\n")
console.log(body.refresh_token)
console.log("\nStore it with:\n")
console.log(
`printf %s "${body.refresh_token}" | gh secret set CHROME_REFRESH_TOKEN`,
)
server.close()
process.exit(0)
})

server.listen(port)