Skip to content

Commit 4d5cb89

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 4d5cb89

4 files changed

Lines changed: 582 additions & 105 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: 69 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,12 @@ const REQUIRED_ENV = [
3636
export function parseArgs(args) {
3737
return {
3838
dryRun: args.includes('--dry-run'),
39+
preflightOnly: args.includes('--preflight-only'),
3940
}
4041
}
4142

4243
export function findMissingEnv(env = process.env) {
43-
return REQUIRED_ENV.filter((name) => !env[name])
44+
return REQUIRED_ENV.filter((name) => String(env[name] ?? '').trim().length === 0)
4445
}
4546

4647
export async function findMissingArtifacts({ exists = fs.pathExists } = {}) {
@@ -156,18 +157,37 @@ export async function updateFirefoxVersionNotes({
156157
}
157158

158159
function resolvePublishExtensionBin() {
159-
const command = process.platform === 'win32' ? 'publish-extension.cmd' : 'publish-extension'
160-
return path.join(process.cwd(), 'node_modules', '.bin', command)
160+
const scriptDir = path.dirname(fileURLToPath(import.meta.url))
161+
return path.join(
162+
scriptDir,
163+
'..',
164+
'node_modules',
165+
'publish-browser-extension',
166+
'bin',
167+
'publish-extension.mjs',
168+
)
169+
}
170+
171+
function buildPublishExtensionEnv(env, baseEnv = process.env) {
172+
const merged = { ...baseEnv, ...(env ?? {}) }
173+
return Object.fromEntries(
174+
Object.entries(merged)
175+
.filter(([, value]) => value !== undefined && value !== null)
176+
.map(([name, value]) => [name, String(value)]),
177+
)
161178
}
162179

163-
async function runPublishExtension(args) {
164-
const command = resolvePublishExtensionBin()
180+
export async function runPublishExtension(
181+
args,
182+
{ env, baseEnv = process.env, spawnImpl = spawn } = {},
183+
) {
184+
const childArgs = [resolvePublishExtensionBin(), ...args]
165185

166186
await new Promise((resolve, reject) => {
167-
const child = spawn(command, args, {
187+
const child = spawnImpl(process.execPath, childArgs, {
168188
stdio: 'inherit',
169189
shell: false,
170-
env: process.env,
190+
env: buildPublishExtensionEnv(env, baseEnv),
171191
})
172192

173193
child.once('error', reject)
@@ -181,34 +201,62 @@ async function runPublishExtension(args) {
181201
})
182202
}
183203

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)
204+
export async function submitStores({
205+
argv = process.argv.slice(2),
206+
env: envInput,
207+
exists = fs.pathExists,
208+
readJson = fs.readJson,
209+
runPublishExtensionImpl = runPublishExtension,
210+
updateFirefoxVersionNotesImpl = updateFirefoxVersionNotes,
211+
logger = console.log,
212+
errorLogger = console.error,
213+
} = {}) {
214+
const { dryRun, preflightOnly } = parseArgs(argv)
215+
const env = envInput ?? process.env
216+
const missingArtifacts = await findMissingArtifacts({ exists })
217+
const missingEnv = preflightOnly ? [] : findMissingEnv(env)
188218

189219
if (missingArtifacts.length > 0 || missingEnv.length > 0) {
190220
if (missingArtifacts.length > 0) {
191-
console.error(`Missing release artifacts: ${missingArtifacts.join(', ')}`)
221+
errorLogger(`Missing release artifacts: ${missingArtifacts.join(', ')}`)
192222
}
193223
if (missingEnv.length > 0) {
194-
console.error(`Missing store submission environment variables: ${missingEnv.join(', ')}`)
224+
errorLogger(`Missing store submission environment variables: ${missingEnv.join(', ')}`)
195225
}
196226
throw new Error('Store submission preflight failed')
197227
}
198228

199-
const manifest = await fs.readJson('build/firefox/manifest.json')
200-
const args = buildPublishExtensionArgs({ dryRun })
229+
let manifest
230+
try {
231+
manifest = await readJson('build/firefox/manifest.json')
232+
} catch (error) {
233+
errorLogger('Missing or invalid Firefox manifest: build/firefox/manifest.json')
234+
throw new Error('Store submission preflight failed', { cause: error })
235+
}
236+
237+
if (!manifest || typeof manifest.version !== 'string' || manifest.version.trim().length === 0) {
238+
errorLogger('Missing Firefox manifest version: build/firefox/manifest.json')
239+
throw new Error('Store submission preflight failed')
240+
}
241+
201242
const firefoxReleaseNotes = buildFirefoxReleaseNotes(manifest.version)
243+
const mode = preflightOnly ? 'preflight' : dryRun ? 'dry-run' : 'submit'
202244

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}`)
245+
logger(`${preflightOnly ? 'Checking' : 'Submitting'} ChatGPTBox ${manifest.version}`)
246+
logger(`Mode: ${mode}`)
247+
logger(`Artifacts: ${REQUIRED_ARTIFACTS.join(', ')}`)
248+
logger(`Firefox version notes: ${firefoxReleaseNotes}`)
207249

208-
await runPublishExtension(args)
250+
if (preflightOnly) {
251+
logger('Store authentication, upload, and submission are skipped in preflight mode')
252+
return
253+
}
254+
255+
const args = buildPublishExtensionArgs({ dryRun })
256+
await runPublishExtensionImpl(args, { env })
209257

210258
if (!dryRun) {
211-
await updateFirefoxVersionNotes({
259+
await updateFirefoxVersionNotesImpl({
212260
extensionId: env.FIREFOX_EXTENSION_ID,
213261
version: manifest.version,
214262
jwtIssuer: env.FIREFOX_JWT_ISSUER,

0 commit comments

Comments
 (0)