Skip to content

Commit 5f4ad50

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 5f4ad50

4 files changed

Lines changed: 503 additions & 105 deletions

File tree

.github/workflows/tagged-release.yml

Lines changed: 87 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -3,123 +3,140 @@ 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_name }}
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 }}"
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+
fi
7786
git push
7887
79-
- if: github.event_name == 'push'
80-
run: |
81-
gh release create ${{github.ref_name}} -d -F CURRENT_CHANGE.md -t ${{github.ref_name}}
88+
- name: Checkout release tag for artifacts
89+
uses: actions/checkout@v7
90+
with:
91+
ref: ${{ github.ref }}
92+
persist-credentials: false
93+
94+
- run: npm ci
95+
96+
- name: Update release artifact manifest.json version
97+
uses: jossef/action-set-json-field@v2.2
98+
with:
99+
file: src/manifest.json
100+
field: version
101+
value: ${{ env.VERSION }}
102+
103+
- name: Update release artifact manifest.v2.json version
104+
uses: jossef/action-set-json-field@v2.2
105+
with:
106+
file: src/manifest.v2.json
107+
field: version
108+
value: ${{ env.VERSION }}
109+
110+
- run: |
111+
gh release create "$RELEASE_TAG" -d -F CURRENT_CHANGE.md -t "$RELEASE_TAG"
112+
env:
113+
GH_TOKEN: ${{ github.token }}
114+
RELEASE_TAG: ${{ github.ref_name }}
82115
83116
- uses: actions/setup-python@v6
84-
if: github.event_name == 'push'
85117
with:
86118
python-version: '3.10' # for appdmg
87119
- uses: maxim-lobanov/setup-xcode@v1
88-
if: github.event_name == 'push'
89120
with:
90121
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
122+
- run: sed -i '' "s/0.0.0/${VERSION}/g" safari/project.pre.patch
123+
- run: sed -i '' "s/0.0.0/${VERSION}/g" safari/project.patch
124+
- run: npm run build:safari
100125

101126
- run: npm run release:firefox-sources
102127

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
128+
- run: |
129+
gh release upload "$RELEASE_TAG" build/chromium.zip
130+
gh release upload "$RELEASE_TAG" build/firefox.zip
131+
gh release upload "$RELEASE_TAG" build/safari.dmg
132+
gh release upload "$RELEASE_TAG" build/chromium-without-katex-and-tiktoken.zip
133+
gh release upload "$RELEASE_TAG" build/firefox-without-katex-and-tiktoken.zip
134+
env:
135+
GH_TOKEN: ${{ github.token }}
136+
RELEASE_TAG: ${{ github.ref_name }}
110137
111138
- 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[@]}"
139+
run: npm run release:submit
123140
env:
124141
CHROME_EXTENSION_ID: ${{ secrets.CHROME_EXTENSION_ID }}
125142
CHROME_CLIENT_ID: ${{ secrets.CHROME_CLIENT_ID }}
@@ -137,6 +154,8 @@ jobs:
137154
EDGE_CLIENT_ID: ${{ secrets.EDGE_CLIENT_ID }}
138155
EDGE_API_KEY: ${{ secrets.EDGE_API_KEY }}
139156

140-
- if: github.event_name == 'push'
141-
run: |
142-
gh release edit ${{github.ref_name}} --draft=false
157+
- run: |
158+
gh release edit "$RELEASE_TAG" --draft=false
159+
env:
160+
GH_TOKEN: ${{ github.token }}
161+
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: 59 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,29 @@ 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).filter(([, value]) => value !== undefined && value !== null),
168+
)
169+
}
170+
171+
export async function runPublishExtension(
172+
args,
173+
{ env, baseEnv = process.env, spawnImpl = spawn, platform = process.platform } = {},
174+
) {
175+
const command = resolvePublishExtensionBin(platform)
165176

166177
await new Promise((resolve, reject) => {
167-
const child = spawn(command, args, {
178+
const child = spawnImpl(command, args, {
168179
stdio: 'inherit',
169-
shell: false,
170-
env: process.env,
180+
shell: platform === 'win32',
181+
env: buildPublishExtensionEnv(env, baseEnv),
171182
})
172183

173184
child.once('error', reject)
@@ -181,34 +192,61 @@ async function runPublishExtension(args) {
181192
})
182193
}
183194

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)
195+
export async function submitStores({
196+
argv = process.argv.slice(2),
197+
env = process.env,
198+
exists = fs.pathExists,
199+
readJson = fs.readJson,
200+
runPublishExtensionImpl = runPublishExtension,
201+
updateFirefoxVersionNotesImpl = updateFirefoxVersionNotes,
202+
logger = console.log,
203+
errorLogger = console.error,
204+
} = {}) {
205+
const { dryRun, preflightOnly } = parseArgs(argv)
206+
const missingArtifacts = await findMissingArtifacts({ exists })
207+
const missingEnv = preflightOnly ? [] : findMissingEnv(env)
188208

189209
if (missingArtifacts.length > 0 || missingEnv.length > 0) {
190210
if (missingArtifacts.length > 0) {
191-
console.error(`Missing release artifacts: ${missingArtifacts.join(', ')}`)
211+
errorLogger(`Missing release artifacts: ${missingArtifacts.join(', ')}`)
192212
}
193213
if (missingEnv.length > 0) {
194-
console.error(`Missing store submission environment variables: ${missingEnv.join(', ')}`)
214+
errorLogger(`Missing store submission environment variables: ${missingEnv.join(', ')}`)
195215
}
196216
throw new Error('Store submission preflight failed')
197217
}
198218

199-
const manifest = await fs.readJson('build/firefox/manifest.json')
200-
const args = buildPublishExtensionArgs({ dryRun })
219+
let manifest
220+
try {
221+
manifest = await readJson('build/firefox/manifest.json')
222+
} catch (error) {
223+
errorLogger('Missing or invalid Firefox manifest: build/firefox/manifest.json')
224+
throw new Error('Store submission preflight failed', { cause: error })
225+
}
226+
227+
if (!manifest || typeof manifest.version !== 'string' || manifest.version.trim().length === 0) {
228+
errorLogger('Missing Firefox manifest version: build/firefox/manifest.json')
229+
throw new Error('Store submission preflight failed')
230+
}
231+
201232
const firefoxReleaseNotes = buildFirefoxReleaseNotes(manifest.version)
233+
const mode = preflightOnly ? 'preflight' : dryRun ? 'dry-run' : 'submit'
202234

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}`)
235+
logger(`${preflightOnly ? 'Checking' : 'Submitting'} ChatGPTBox ${manifest.version}`)
236+
logger(`Mode: ${mode}`)
237+
logger(`Artifacts: ${REQUIRED_ARTIFACTS.join(', ')}`)
238+
logger(`Firefox version notes: ${firefoxReleaseNotes}`)
207239

208-
await runPublishExtension(args)
240+
if (preflightOnly) {
241+
logger('Store authentication, upload, and submission are skipped in preflight mode')
242+
return
243+
}
244+
245+
const args = buildPublishExtensionArgs({ dryRun })
246+
await runPublishExtensionImpl(args, { env })
209247

210248
if (!dryRun) {
211-
await updateFirefoxVersionNotes({
249+
await updateFirefoxVersionNotesImpl({
212250
extensionId: env.FIREFOX_EXTENSION_ID,
213251
version: manifest.version,
214252
jwtIssuer: env.FIREFOX_JWT_ISSUER,

0 commit comments

Comments
 (0)