Skip to content

Commit 18d1963

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 18d1963

4 files changed

Lines changed: 560 additions & 106 deletions

File tree

.github/workflows/tagged-release.yml

Lines changed: 90 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -3,123 +3,142 @@ 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+
persist-credentials: true
54+
55+
- uses: actions/setup-node@v6
56+
with:
57+
node-version: 22
58+
4559
- 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
60+
run: printf 'VERSION=%s\n' "${GITHUB_REF_NAME#v}" >> "$GITHUB_ENV"
5361

5462
- name: Update manifest.json version
55-
if: github.event_name == 'push'
5663
uses: jossef/action-set-json-field@v2.2
5764
with:
5865
file: src/manifest.json
5966
field: version
6067
value: ${{ env.VERSION }}
6168

6269
- name: Update manifest.v2.json version
63-
if: github.event_name == 'push'
6470
uses: jossef/action-set-json-field@v2.2
6571
with:
6672
file: src/manifest.v2.json
6773
field: version
6874
value: ${{ env.VERSION }}
6975

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

101128
- run: npm run release:firefox-sources
102129

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
130+
- run: |
131+
gh release upload "$RELEASE_TAG" build/chromium.zip
132+
gh release upload "$RELEASE_TAG" build/firefox.zip
133+
gh release upload "$RELEASE_TAG" build/safari.dmg
134+
gh release upload "$RELEASE_TAG" build/chromium-without-katex-and-tiktoken.zip
135+
gh release upload "$RELEASE_TAG" build/firefox-without-katex-and-tiktoken.zip
136+
env:
137+
GH_TOKEN: ${{ github.token }}
138+
RELEASE_TAG: ${{ github.ref_name }}
110139
111140
- 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[@]}"
141+
run: npm run release:submit
123142
env:
124143
CHROME_EXTENSION_ID: ${{ secrets.CHROME_EXTENSION_ID }}
125144
CHROME_CLIENT_ID: ${{ secrets.CHROME_CLIENT_ID }}
@@ -137,6 +156,8 @@ jobs:
137156
EDGE_CLIENT_ID: ${{ secrets.EDGE_CLIENT_ID }}
138157
EDGE_API_KEY: ${{ secrets.EDGE_API_KEY }}
139158

140-
- if: github.event_name == 'push'
141-
run: |
142-
gh release edit ${{github.ref_name}} --draft=false
159+
- run: |
160+
gh release edit "$RELEASE_TAG" --draft=false
161+
env:
162+
GH_TOKEN: ${{ github.token }}
163+
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: 63 additions & 21 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

@@ -155,19 +156,32 @@ export async function updateFirefoxVersionNotes({
155156
}
156157
}
157158

158-
function resolvePublishExtensionBin() {
159-
const command = process.platform === 'win32' ? 'publish-extension.cmd' : 'publish-extension'
159+
function resolvePublishExtensionBin(platform = process.platform) {
160+
const command = platform === 'win32' ? 'publish-extension.cmd' : 'publish-extension'
160161
return path.join(process.cwd(), 'node_modules', '.bin', command)
161162
}
162163

163-
async function runPublishExtension(args) {
164-
const command = resolvePublishExtensionBin()
164+
function buildPublishExtensionEnv(env, baseEnv = process.env) {
165+
const merged = { ...baseEnv, ...(env ?? {}) }
166+
return Object.fromEntries(
167+
Object.entries(merged)
168+
.filter(([, value]) => value !== undefined && value !== null)
169+
.map(([name, value]) => [name, String(value)]),
170+
)
171+
}
172+
173+
export async function runPublishExtension(
174+
args,
175+
{ env, baseEnv = process.env, spawnImpl = spawn, platform = process.platform } = {},
176+
) {
177+
const bin = resolvePublishExtensionBin(platform)
178+
const command = platform === 'win32' ? `"${bin}"` : bin
165179

166180
await new Promise((resolve, reject) => {
167-
const child = spawn(command, args, {
181+
const child = spawnImpl(command, args, {
168182
stdio: 'inherit',
169-
shell: false,
170-
env: process.env,
183+
shell: platform === 'win32',
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'
202238

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}`)
239+
logger(`${preflightOnly ? 'Checking' : 'Submitting'} ChatGPTBox ${manifest.version}`)
240+
logger(`Mode: ${mode}`)
241+
logger(`Artifacts: ${REQUIRED_ARTIFACTS.join(', ')}`)
242+
logger(`Firefox version notes: ${firefoxReleaseNotes}`)
207243

208-
await runPublishExtension(args)
244+
if (preflightOnly) {
245+
logger('Store authentication, upload, and submission are skipped in preflight mode')
246+
return
247+
}
248+
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)