Skip to content

Commit e08da38

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, and credential boundaries with targeted tests.
1 parent ff3d5f7 commit e08da38

4 files changed

Lines changed: 369 additions & 99 deletions

File tree

.github/workflows/tagged-release.yml

Lines changed: 61 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -3,123 +3,115 @@ 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'
20+
runs-on: macos-14
21+
permissions:
22+
contents: read
23+
24+
steps:
25+
- uses: actions/checkout@v7
26+
with:
27+
ref: ${{ github.ref_name }}
28+
persist-credentials: false
29+
30+
- uses: actions/setup-node@v6
31+
with:
32+
node-version: 22
33+
- run: npm ci
34+
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'
3345
runs-on: macos-14
46+
permissions:
47+
contents: write
3448

3549
steps:
3650
- uses: actions/checkout@v7
3751
with:
38-
ref: ${{ github.event_name == 'push' && 'master' || github.ref_name }}
52+
ref: master
53+
persist-credentials: true
3954

4055
- uses: actions/setup-node@v6
4156
with:
4257
node-version: 22
4358
- run: npm ci
4459

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'
7278
continue-on-error: true
7379
run: |
7480
git config --global user.email "github-actions[bot]@users.noreply.github.com"
7581
git config --global user.name "github-actions[bot]"
76-
git commit -am "release v${{ env.VERSION }}"
82+
git commit -am "release v${VERSION}"
7783
git push
7884
79-
- if: github.event_name == 'push'
80-
run: |
81-
gh release create ${{github.ref_name}} -d -F CURRENT_CHANGE.md -t ${{github.ref_name}}
85+
- run: |
86+
gh release create "$RELEASE_TAG" -d -F CURRENT_CHANGE.md -t "$RELEASE_TAG"
87+
env:
88+
GH_TOKEN: ${{ github.token }}
89+
RELEASE_TAG: ${{ github.ref_name }}
8290
8391
- uses: actions/setup-python@v6
84-
if: github.event_name == 'push'
8592
with:
8693
python-version: '3.10' # for appdmg
8794
- uses: maxim-lobanov/setup-xcode@v1
88-
if: github.event_name == 'push'
8995
with:
9096
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
97+
- run: sed -i '' "s/0.0.0/${VERSION}/g" safari/project.pre.patch
98+
- run: sed -i '' "s/0.0.0/${VERSION}/g" safari/project.patch
99+
- run: npm run build:safari
100100

101101
- run: npm run release:firefox-sources
102102

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
103+
- run: |
104+
gh release upload "$RELEASE_TAG" build/chromium.zip
105+
gh release upload "$RELEASE_TAG" build/firefox.zip
106+
gh release upload "$RELEASE_TAG" build/safari.dmg
107+
gh release upload "$RELEASE_TAG" build/chromium-without-katex-and-tiktoken.zip
108+
gh release upload "$RELEASE_TAG" build/firefox-without-katex-and-tiktoken.zip
109+
env:
110+
GH_TOKEN: ${{ github.token }}
111+
RELEASE_TAG: ${{ github.ref_name }}
110112
111113
- 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[@]}"
114+
run: npm run release:submit
123115
env:
124116
CHROME_EXTENSION_ID: ${{ secrets.CHROME_EXTENSION_ID }}
125117
CHROME_CLIENT_ID: ${{ secrets.CHROME_CLIENT_ID }}
@@ -137,6 +129,8 @@ jobs:
137129
EDGE_CLIENT_ID: ${{ secrets.EDGE_CLIENT_ID }}
138130
EDGE_API_KEY: ${{ secrets.EDGE_API_KEY }}
139131

140-
- if: github.event_name == 'push'
141-
run: |
142-
gh release edit ${{github.ref_name}} --draft=false
132+
- run: |
133+
gh release edit "$RELEASE_TAG" --draft=false
134+
env:
135+
GH_TOKEN: ${{ github.token }}
136+
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: 44 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ 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

@@ -160,14 +161,14 @@ function resolvePublishExtensionBin() {
160161
return path.join(process.cwd(), 'node_modules', '.bin', command)
161162
}
162163

163-
async function runPublishExtension(args) {
164+
async function runPublishExtension(args, { env = process.env } = {}) {
164165
const command = resolvePublishExtensionBin()
165166

166167
await new Promise((resolve, reject) => {
167168
const child = spawn(command, args, {
168169
stdio: 'inherit',
169170
shell: false,
170-
env: process.env,
171+
env,
171172
})
172173

173174
child.once('error', reject)
@@ -181,34 +182,61 @@ async function runPublishExtension(args) {
181182
})
182183
}
183184

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)
185+
export async function submitStores({
186+
argv = process.argv.slice(2),
187+
env = process.env,
188+
exists = fs.pathExists,
189+
readJson = fs.readJson,
190+
runPublishExtensionImpl = runPublishExtension,
191+
updateFirefoxVersionNotesImpl = updateFirefoxVersionNotes,
192+
logger = console.log,
193+
errorLogger = console.error,
194+
} = {}) {
195+
const { dryRun, preflightOnly } = parseArgs(argv)
196+
const missingArtifacts = await findMissingArtifacts({ exists })
197+
const missingEnv = preflightOnly ? [] : findMissingEnv(env)
188198

189199
if (missingArtifacts.length > 0 || missingEnv.length > 0) {
190200
if (missingArtifacts.length > 0) {
191-
console.error(`Missing release artifacts: ${missingArtifacts.join(', ')}`)
201+
errorLogger(`Missing release artifacts: ${missingArtifacts.join(', ')}`)
192202
}
193203
if (missingEnv.length > 0) {
194-
console.error(`Missing store submission environment variables: ${missingEnv.join(', ')}`)
204+
errorLogger(`Missing store submission environment variables: ${missingEnv.join(', ')}`)
195205
}
196206
throw new Error('Store submission preflight failed')
197207
}
198208

199-
const manifest = await fs.readJson('build/firefox/manifest.json')
200-
const args = buildPublishExtensionArgs({ dryRun })
209+
let manifest
210+
try {
211+
manifest = await readJson('build/firefox/manifest.json')
212+
} catch (error) {
213+
errorLogger('Missing or invalid Firefox manifest: build/firefox/manifest.json')
214+
throw new Error('Store submission preflight failed', { cause: error })
215+
}
216+
217+
if (!manifest || typeof manifest.version !== 'string' || manifest.version.trim().length === 0) {
218+
errorLogger('Missing Firefox manifest version: build/firefox/manifest.json')
219+
throw new Error('Store submission preflight failed')
220+
}
221+
201222
const firefoxReleaseNotes = buildFirefoxReleaseNotes(manifest.version)
223+
const mode = preflightOnly ? 'preflight' : dryRun ? 'dry-run' : 'submit'
202224

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}`)
225+
logger(`${preflightOnly ? 'Checking' : 'Submitting'} ChatGPTBox ${manifest.version}`)
226+
logger(`Mode: ${mode}`)
227+
logger(`Artifacts: ${REQUIRED_ARTIFACTS.join(', ')}`)
228+
logger(`Firefox version notes: ${firefoxReleaseNotes}`)
207229

208-
await runPublishExtension(args)
230+
if (preflightOnly) {
231+
logger('Store authentication, upload, and submission are skipped in preflight mode')
232+
return
233+
}
234+
235+
const args = buildPublishExtensionArgs({ dryRun })
236+
await runPublishExtensionImpl(args, { env })
209237

210238
if (!dryRun) {
211-
await updateFirefoxVersionNotes({
239+
await updateFirefoxVersionNotesImpl({
212240
extensionId: env.FIREFOX_EXTENSION_ID,
213241
version: manifest.version,
214242
jwtIssuer: env.FIREFOX_JWT_ISSUER,

0 commit comments

Comments
 (0)