Skip to content

Commit 369b23e

Browse files
Harden GitHub Actions manual store submission with secret-free preflight
Manual dispatches should validate release artifacts without invoking publish-browser-extension's authenticated dry-run path. Run workflow_dispatch as a read-only GitHub Actions preflight job with no persisted checkout credentials. Keep real store submission on tag pushes only. Cover artifact, manifest shape, process environment, and credential boundaries with targeted tests.
1 parent ff3d5f7 commit 369b23e

4 files changed

Lines changed: 575 additions & 106 deletions

File tree

.github/workflows/tagged-release.yml

Lines changed: 91 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -3,123 +3,143 @@ on:
33
workflow_dispatch:
44
inputs:
55
submit_stores:
6-
description: "Run Chrome, Firefox, and Edge store submission preflight"
6+
description: 'Run store submission artifact preflight without store credentials'
77
required: false
8-
default: "false"
8+
default: 'false'
99
type: choice
1010
options:
11-
- "false"
12-
- "true"
13-
dry_run:
14-
description: "Validate store submission without uploading artifacts"
15-
required: false
16-
default: "true"
17-
type: choice
18-
options:
19-
- "true"
20-
- "false"
11+
- 'false'
12+
- 'true'
2113
push:
2214
tags:
23-
- "v*"
24-
25-
permissions:
26-
id-token: "write"
27-
contents: "write"
28-
env:
29-
GH_TOKEN: ${{ github.token }}
15+
- 'v*'
3016

3117
jobs:
32-
build_and_release:
18+
manual_preflight:
19+
if: github.event_name == 'workflow_dispatch'
3320
runs-on: macos-14
21+
permissions:
22+
contents: read
3423

3524
steps:
3625
- uses: actions/checkout@v7
3726
with:
38-
ref: ${{ github.event_name == 'push' && 'master' || github.ref_name }}
27+
ref: ${{ github.ref }}
28+
persist-credentials: false
3929

4030
- uses: actions/setup-node@v6
4131
with:
4232
node-version: 22
4333
- run: npm ci
4434

35+
- run: npm run build
36+
37+
- run: npm run release:firefox-sources
38+
39+
- name: Submit stores preflight
40+
if: inputs.submit_stores == 'true'
41+
run: npm run release:submit:preflight
42+
43+
release:
44+
if: github.event_name == 'push'
45+
runs-on: macos-14
46+
permissions:
47+
contents: write
48+
49+
steps:
50+
- uses: actions/checkout@v7
51+
with:
52+
ref: master
53+
fetch-depth: 0
54+
persist-credentials: true
55+
56+
- uses: actions/setup-node@v6
57+
with:
58+
node-version: 22
59+
4560
- name: Resolve release version
46-
run: |
47-
if [ "${{ github.event_name }}" = "push" ]; then
48-
echo "VERSION=${GITHUB_REF_NAME#v}" >> $GITHUB_ENV
49-
else
50-
version="$(node -p "require('./src/manifest.json').version")"
51-
echo "VERSION=${version}" >> $GITHUB_ENV
52-
fi
61+
run: printf 'VERSION=%s\n' "${GITHUB_REF_NAME#v}" >> "$GITHUB_ENV"
5362

5463
- name: Update manifest.json version
55-
if: github.event_name == 'push'
5664
uses: jossef/action-set-json-field@v2.2
5765
with:
5866
file: src/manifest.json
5967
field: version
6068
value: ${{ env.VERSION }}
6169

6270
- name: Update manifest.v2.json version
63-
if: github.event_name == 'push'
6471
uses: jossef/action-set-json-field@v2.2
6572
with:
6673
file: src/manifest.v2.json
6774
field: version
6875
value: ${{ env.VERSION }}
6976

7077
- name: Push files
71-
if: github.event_name == 'push'
72-
continue-on-error: true
7378
run: |
7479
git config --global user.email "github-actions[bot]@users.noreply.github.com"
7580
git config --global user.name "github-actions[bot]"
76-
git commit -am "release v${{ env.VERSION }}"
77-
git push
81+
git add src/manifest.json src/manifest.v2.json
82+
if git diff --cached --quiet; then
83+
echo "No release version changes to commit"
84+
else
85+
git commit -m "release v${VERSION}"
86+
git fetch origin master
87+
git rebase FETCH_HEAD
88+
git push origin HEAD:master
89+
fi
7890
79-
- if: github.event_name == 'push'
80-
run: |
81-
gh release create ${{github.ref_name}} -d -F CURRENT_CHANGE.md -t ${{github.ref_name}}
91+
- name: Checkout release tag for artifacts
92+
uses: actions/checkout@v7
93+
with:
94+
ref: ${{ github.ref }}
95+
persist-credentials: false
96+
97+
- run: npm ci
98+
99+
- name: Update release artifact manifest.json version
100+
uses: jossef/action-set-json-field@v2.2
101+
with:
102+
file: src/manifest.json
103+
field: version
104+
value: ${{ env.VERSION }}
105+
106+
- name: Update release artifact manifest.v2.json version
107+
uses: jossef/action-set-json-field@v2.2
108+
with:
109+
file: src/manifest.v2.json
110+
field: version
111+
value: ${{ env.VERSION }}
112+
113+
- run: |
114+
gh release create "$RELEASE_TAG" -d -F CURRENT_CHANGE.md -t "$RELEASE_TAG"
115+
env:
116+
GH_TOKEN: ${{ github.token }}
117+
RELEASE_TAG: ${{ github.ref_name }}
82118
83119
- uses: actions/setup-python@v6
84-
if: github.event_name == 'push'
85120
with:
86121
python-version: '3.10' # for appdmg
87122
- uses: maxim-lobanov/setup-xcode@v1
88-
if: github.event_name == 'push'
89123
with:
90124
xcode-version: 16.2
91-
- if: github.event_name == 'push'
92-
run: sed -i '' "s/0.0.0/${{ env.VERSION }}/g" safari/project.pre.patch
93-
- if: github.event_name == 'push'
94-
run: sed -i '' "s/0.0.0/${{ env.VERSION }}/g" safari/project.patch
95-
- if: github.event_name == 'push'
96-
run: npm run build:safari
97-
98-
- if: github.event_name != 'push'
99-
run: npm run build
125+
- run: sed -i '' "s/0.0.0/${VERSION}/g" safari/project.pre.patch
126+
- run: sed -i '' "s/0.0.0/${VERSION}/g" safari/project.patch
127+
- run: npm run build:safari
100128

101129
- run: npm run release:firefox-sources
102130

103-
- if: github.event_name == 'push'
104-
run: |
105-
gh release upload ${{github.ref_name}} build/chromium.zip
106-
gh release upload ${{github.ref_name}} build/firefox.zip
107-
gh release upload ${{github.ref_name}} build/safari.dmg
108-
gh release upload ${{github.ref_name}} build/chromium-without-katex-and-tiktoken.zip
109-
gh release upload ${{github.ref_name}} build/firefox-without-katex-and-tiktoken.zip
131+
- run: |
132+
gh release upload "$RELEASE_TAG" build/chromium.zip
133+
gh release upload "$RELEASE_TAG" build/firefox.zip
134+
gh release upload "$RELEASE_TAG" build/safari.dmg
135+
gh release upload "$RELEASE_TAG" build/chromium-without-katex-and-tiktoken.zip
136+
gh release upload "$RELEASE_TAG" build/firefox-without-katex-and-tiktoken.zip
137+
env:
138+
GH_TOKEN: ${{ github.token }}
139+
RELEASE_TAG: ${{ github.ref_name }}
110140
111141
- name: Submit stores
112-
if: github.event_name == 'push' || inputs.submit_stores == 'true'
113-
run: |
114-
args=()
115-
if [ "${{ github.event_name }}" != "push" ]; then
116-
if [ "${{ inputs.dry_run }}" != "true" ]; then
117-
echo "::error::Manual store submission only supports dry_run=true. Push a v* tag for a real submission."
118-
exit 1
119-
fi
120-
args+=(--dry-run)
121-
fi
122-
npm run release:submit -- "${args[@]}"
142+
run: npm run release:submit
123143
env:
124144
CHROME_EXTENSION_ID: ${{ secrets.CHROME_EXTENSION_ID }}
125145
CHROME_CLIENT_ID: ${{ secrets.CHROME_CLIENT_ID }}
@@ -137,6 +157,8 @@ jobs:
137157
EDGE_CLIENT_ID: ${{ secrets.EDGE_CLIENT_ID }}
138158
EDGE_API_KEY: ${{ secrets.EDGE_API_KEY }}
139159

140-
- if: github.event_name == 'push'
141-
run: |
142-
gh release edit ${{github.ref_name}} --draft=false
160+
- run: |
161+
gh release edit "$RELEASE_TAG" --draft=false
162+
env:
163+
GH_TOKEN: ${{ github.token }}
164+
RELEASE_TAG: ${{ github.ref_name }}

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"release:firefox-sources": "node scripts/create-firefox-sources-zip.mjs",
2121
"release:submit": "node scripts/submit-stores.mjs",
2222
"release:submit:dry-run": "node scripts/submit-stores.mjs --dry-run",
23+
"release:submit:preflight": "node scripts/submit-stores.mjs --preflight-only",
2324
"release:update-firefox-metadata": "node scripts/update-firefox-metadata.mjs",
2425
"release:check-edge-api-key": "node scripts/check-edge-api-key-expiry.mjs"
2526
},

scripts/submit-stores.mjs

Lines changed: 64 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@
33
import fs from 'fs-extra'
44
import jwt from 'jsonwebtoken'
55
import { spawn } from 'node:child_process'
6-
import path from 'node:path'
76
import { randomUUID } from 'node:crypto'
7+
import { createRequire } from 'node:module'
8+
import path from 'node:path'
89
import { fileURLToPath } from 'node:url'
910

1011
const REQUIRED_ARTIFACTS = ['build/chromium.zip', 'build/firefox.zip', 'build/firefox-sources.zip']
1112
const AMO_BASE_URL = 'https://addons.mozilla.org'
13+
const require = createRequire(import.meta.url)
1214
export const FIREFOX_COMPATIBILITY = {
1315
firefox: {
1416
min: '58.0',
@@ -36,11 +38,12 @@ const REQUIRED_ENV = [
3638
export function parseArgs(args) {
3739
return {
3840
dryRun: args.includes('--dry-run'),
41+
preflightOnly: args.includes('--preflight-only'),
3942
}
4043
}
4144

4245
export function findMissingEnv(env = process.env) {
43-
return REQUIRED_ENV.filter((name) => !env[name])
46+
return REQUIRED_ENV.filter((name) => String(env[name] ?? '').trim().length === 0)
4447
}
4548

4649
export async function findMissingArtifacts({ exists = fs.pathExists } = {}) {
@@ -156,18 +159,29 @@ export async function updateFirefoxVersionNotes({
156159
}
157160

158161
function resolvePublishExtensionBin() {
159-
const command = process.platform === 'win32' ? 'publish-extension.cmd' : 'publish-extension'
160-
return path.join(process.cwd(), 'node_modules', '.bin', command)
162+
return require.resolve('publish-browser-extension/cli')
163+
}
164+
165+
function buildPublishExtensionEnv(env, baseEnv = process.env) {
166+
const merged = { ...baseEnv, ...(env ?? {}) }
167+
return Object.fromEntries(
168+
Object.entries(merged)
169+
.filter(([, value]) => value !== undefined && value !== null)
170+
.map(([name, value]) => [name, String(value)]),
171+
)
161172
}
162173

163-
async function runPublishExtension(args) {
164-
const command = resolvePublishExtensionBin()
174+
export async function runPublishExtension(
175+
args,
176+
{ env, baseEnv = process.env, spawnImpl = spawn } = {},
177+
) {
178+
const childArgs = [resolvePublishExtensionBin(), ...args]
165179

166180
await new Promise((resolve, reject) => {
167-
const child = spawn(command, args, {
181+
const child = spawnImpl(process.execPath, childArgs, {
168182
stdio: 'inherit',
169183
shell: false,
170-
env: process.env,
184+
env: buildPublishExtensionEnv(env, baseEnv),
171185
})
172186

173187
child.once('error', reject)
@@ -181,34 +195,62 @@ async function runPublishExtension(args) {
181195
})
182196
}
183197

184-
export async function submitStores({ argv = process.argv.slice(2), env = process.env } = {}) {
185-
const { dryRun } = parseArgs(argv)
186-
const missingArtifacts = await findMissingArtifacts()
187-
const missingEnv = findMissingEnv(env)
198+
export async function submitStores({
199+
argv = process.argv.slice(2),
200+
env: envInput,
201+
exists = fs.pathExists,
202+
readJson = fs.readJson,
203+
runPublishExtensionImpl = runPublishExtension,
204+
updateFirefoxVersionNotesImpl = updateFirefoxVersionNotes,
205+
logger = console.log,
206+
errorLogger = console.error,
207+
} = {}) {
208+
const { dryRun, preflightOnly } = parseArgs(argv)
209+
const env = envInput ?? process.env
210+
const missingArtifacts = await findMissingArtifacts({ exists })
211+
const missingEnv = preflightOnly ? [] : findMissingEnv(env)
188212

189213
if (missingArtifacts.length > 0 || missingEnv.length > 0) {
190214
if (missingArtifacts.length > 0) {
191-
console.error(`Missing release artifacts: ${missingArtifacts.join(', ')}`)
215+
errorLogger(`Missing release artifacts: ${missingArtifacts.join(', ')}`)
192216
}
193217
if (missingEnv.length > 0) {
194-
console.error(`Missing store submission environment variables: ${missingEnv.join(', ')}`)
218+
errorLogger(`Missing store submission environment variables: ${missingEnv.join(', ')}`)
195219
}
196220
throw new Error('Store submission preflight failed')
197221
}
198222

199-
const manifest = await fs.readJson('build/firefox/manifest.json')
200-
const args = buildPublishExtensionArgs({ dryRun })
223+
let manifest
224+
try {
225+
manifest = await readJson('build/firefox/manifest.json')
226+
} catch (error) {
227+
errorLogger('Missing or invalid Firefox manifest: build/firefox/manifest.json')
228+
throw new Error('Store submission preflight failed', { cause: error })
229+
}
230+
231+
if (!manifest || typeof manifest.version !== 'string' || manifest.version.trim().length === 0) {
232+
errorLogger('Missing Firefox manifest version: build/firefox/manifest.json')
233+
throw new Error('Store submission preflight failed')
234+
}
235+
201236
const firefoxReleaseNotes = buildFirefoxReleaseNotes(manifest.version)
237+
const mode = preflightOnly ? 'preflight' : dryRun ? 'dry-run' : 'submit'
238+
239+
logger(`${preflightOnly ? 'Checking' : 'Submitting'} ChatGPTBox ${manifest.version}`)
240+
logger(`Mode: ${mode}`)
241+
logger(`Artifacts: ${REQUIRED_ARTIFACTS.join(', ')}`)
242+
logger(`Firefox version notes: ${firefoxReleaseNotes}`)
202243

203-
console.log(`Submitting ChatGPTBox ${manifest.version} to Chrome, Firefox, and Edge`)
204-
console.log(`Mode: ${dryRun ? 'dry-run' : 'submit'}`)
205-
console.log(`Artifacts: ${REQUIRED_ARTIFACTS.join(', ')}`)
206-
console.log(`Firefox version notes: ${firefoxReleaseNotes}`)
244+
if (preflightOnly) {
245+
logger('Store authentication, upload, and submission are skipped in preflight mode')
246+
return
247+
}
207248

208-
await runPublishExtension(args)
249+
const args = buildPublishExtensionArgs({ dryRun })
250+
await runPublishExtensionImpl(args, { env })
209251

210252
if (!dryRun) {
211-
await updateFirefoxVersionNotes({
253+
await updateFirefoxVersionNotesImpl({
212254
extensionId: env.FIREFOX_EXTENSION_ID,
213255
version: manifest.version,
214256
jwtIssuer: env.FIREFOX_JWT_ISSUER,

0 commit comments

Comments
 (0)