Skip to content

Commit 7cfbd36

Browse files
committed
ci: automate extension releases via changesets + tag-triggered workflow
Removes @playhtml/extension from the changesets ignore list so extension changes get tracked alongside core-package changes. The package stays private:true, so changeset publish skips npm but still emits a @playhtml/extension@x.y.z tag. Adds .github/workflows/extension-release.yml that fires on those tags, builds Chrome + Firefox zips, and runs wxt submit with credentials from GitHub secrets. workflow_dispatch supports a dry-run toggle for testing without actually submitting. Documents required secrets and the OAuth Desktop-app client requirement (Web app clients trigger Google's 7-day token expiry).
1 parent 7d9fb18 commit 7cfbd36

3 files changed

Lines changed: 154 additions & 1 deletion

File tree

.changeset/config.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"access": "public",
88
"baseBranch": "main",
99
"updateInternalDependencies": "patch",
10-
"ignore": ["@playhtml/extension", "@playhtml/extension-worker", "wewere-online", "@playhtml/docs-site"],
10+
"ignore": ["@playhtml/extension-worker", "wewere-online", "@playhtml/docs-site"],
1111
"snapshot": {
1212
"useCalculatedVersion": true,
1313
"prereleaseTemplate": "{tag}-{commit}"
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
name: Extension Release
2+
3+
on:
4+
push:
5+
tags:
6+
- "@playhtml/extension@*"
7+
workflow_dispatch:
8+
inputs:
9+
dry-run:
10+
description: "Dry run (build + validate, do not submit)"
11+
type: boolean
12+
default: true
13+
skip-chrome:
14+
description: "Skip Chrome submission"
15+
type: boolean
16+
default: false
17+
skip-firefox:
18+
description: "Skip Firefox submission"
19+
type: boolean
20+
default: false
21+
22+
concurrency: ${{ github.workflow }}-${{ github.ref }}
23+
24+
permissions:
25+
contents: read
26+
27+
jobs:
28+
submit:
29+
name: Build and submit to stores
30+
runs-on: ubuntu-latest
31+
env:
32+
DRY_RUN: ${{ inputs.dry-run && 'true' || 'false' }}
33+
SKIP_CHROME: ${{ inputs.skip-chrome && 'true' || 'false' }}
34+
SKIP_FIREFOX: ${{ inputs.skip-firefox && 'true' || 'false' }}
35+
steps:
36+
- name: Checkout repo
37+
uses: actions/checkout@v4
38+
39+
- name: Setup Node.js
40+
uses: actions/setup-node@v4
41+
with:
42+
node-version: 24
43+
44+
- name: Setup Bun
45+
uses: oven-sh/setup-bun@v2
46+
with:
47+
bun-version: latest
48+
49+
- name: Install dependencies
50+
run: bun install
51+
52+
- name: Build packages
53+
run: bun run build-packages
54+
55+
- name: Read extension version
56+
id: version
57+
run: echo "version=$(node -p "require('./extension/package.json').version")" >> "$GITHUB_OUTPUT"
58+
59+
- name: Build Chrome zip
60+
if: env.SKIP_CHROME != 'true'
61+
run: WXT_OUT_DIR=publish bun run -C extension wxt zip
62+
63+
- name: Build Firefox zip
64+
if: env.SKIP_FIREFOX != 'true'
65+
run: WXT_OUT_DIR=publish bun run -C extension wxt zip -b firefox
66+
67+
- name: Resolve zip paths
68+
id: zips
69+
working-directory: extension
70+
env:
71+
VERSION: ${{ steps.version.outputs.version }}
72+
run: |
73+
if [ -d publish ]; then
74+
CHROME_ZIP=$(ls publish/*-"$VERSION"-chrome.zip 2>/dev/null | head -1 || true)
75+
FIREFOX_ZIP=$(ls publish/*-"$VERSION"-firefox.zip 2>/dev/null | head -1 || true)
76+
SOURCES_ZIP=$(ls publish/*-"$VERSION"-sources.zip 2>/dev/null | head -1 || true)
77+
echo "chrome=$CHROME_ZIP" >> "$GITHUB_OUTPUT"
78+
echo "firefox=$FIREFOX_ZIP" >> "$GITHUB_OUTPUT"
79+
echo "sources=$SOURCES_ZIP" >> "$GITHUB_OUTPUT"
80+
echo "Built artifacts:"
81+
ls -la publish/
82+
fi
83+
84+
- name: Submit to stores
85+
working-directory: extension
86+
env:
87+
CHROME_EXTENSION_ID: ${{ secrets.CHROME_EXTENSION_ID }}
88+
CHROME_CLIENT_ID: ${{ secrets.CHROME_CLIENT_ID }}
89+
CHROME_CLIENT_SECRET: ${{ secrets.CHROME_CLIENT_SECRET }}
90+
CHROME_REFRESH_TOKEN: ${{ secrets.CHROME_REFRESH_TOKEN }}
91+
FIREFOX_EXTENSION_ID: ${{ secrets.FIREFOX_EXTENSION_ID }}
92+
FIREFOX_JWT_ISSUER: ${{ secrets.FIREFOX_JWT_ISSUER }}
93+
FIREFOX_JWT_SECRET: ${{ secrets.FIREFOX_JWT_SECRET }}
94+
CHROME_ZIP: ${{ steps.zips.outputs.chrome }}
95+
FIREFOX_ZIP: ${{ steps.zips.outputs.firefox }}
96+
SOURCES_ZIP: ${{ steps.zips.outputs.sources }}
97+
run: |
98+
ARGS=()
99+
if [ "$SKIP_CHROME" != "true" ] && [ -n "$CHROME_ZIP" ]; then
100+
ARGS+=(--chrome-zip "$CHROME_ZIP")
101+
fi
102+
if [ "$SKIP_FIREFOX" != "true" ] && [ -n "$FIREFOX_ZIP" ]; then
103+
ARGS+=(--firefox-zip "$FIREFOX_ZIP" --firefox-sources-zip "$SOURCES_ZIP")
104+
fi
105+
if [ "$DRY_RUN" = "true" ]; then
106+
bun run wxt submit --dry-run "${ARGS[@]}"
107+
else
108+
bun run wxt submit "${ARGS[@]}"
109+
fi
110+
111+
- name: Upload zips as artifacts
112+
if: always()
113+
uses: actions/upload-artifact@v4
114+
with:
115+
name: extension-${{ steps.version.outputs.version }}
116+
path: extension/publish/*.zip
117+
retention-days: 30
118+
if-no-files-found: warn

extension/CLAUDE.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,41 @@ Worker backend (in `worker/`):
1515
- `cd worker && wrangler dev`: Local API server (localhost:8787)
1616
- `cd worker && wrangler deploy`: Deploy to Cloudflare
1717

18+
## Releases
19+
20+
The extension uses the same Changesets flow as the core packages. Add a
21+
`.changeset/<slug>.md` file describing user-facing changes (frontmatter:
22+
`"@playhtml/extension": patch|minor|major`). Changesets bumps the version,
23+
updates `extension/CHANGELOG.md`, and pushes a tag (`@playhtml/extension@x.y.z`)
24+
when the "Release: Version packages" PR merges. Because the package is
25+
`private: true`, `changeset publish` skips npm — the tag is the trigger.
26+
27+
The tag push fires `.github/workflows/extension-release.yml`, which builds
28+
Chrome + Firefox zips and runs `wxt submit` for both stores.
29+
30+
**Required GitHub Actions secrets** (set once at repo level):
31+
32+
Chrome Web Store:
33+
- `CHROME_EXTENSION_ID`
34+
- `CHROME_CLIENT_ID` — OAuth Desktop-app client (NOT Web app — Web clients use
35+
the deprecated OOB flow which Google penalizes with ~7-day token expiry)
36+
- `CHROME_CLIENT_SECRET`
37+
- `CHROME_REFRESH_TOKEN` — generated once via OAuth Playground, scope
38+
`https://www.googleapis.com/auth/chromewebstore`. Long-lived but can be
39+
invalidated by Google account password change, 6-month inactivity, or
40+
security events. Regenerate locally with `bun run submit:refresh-chrome-token`
41+
and update the secret if CI fails with an OAuth error.
42+
43+
Firefox AMO:
44+
- `FIREFOX_EXTENSION_ID`
45+
- `FIREFOX_JWT_ISSUER` — from addons.mozilla.org → Developer Hub → Manage API Keys
46+
- `FIREFOX_JWT_SECRET`
47+
48+
**Manual fallback / testing:** The workflow also supports `workflow_dispatch`
49+
with a `dry-run` toggle (defaults to true) — run it from the Actions tab to
50+
verify zips build and credentials work without actually submitting. The local
51+
`./release.sh` continues to work as a manual escape hatch.
52+
1853
## Website & experiments (`extension/website/`)
1954

2055
The `extension/website/` Vite app serves both the marketing/landing pages

0 commit comments

Comments
 (0)