From 91235120c99c7b15e91faad1867c9b7de49b23b8 Mon Sep 17 00:00:00 2001 From: Kevin Buffardi Date: Thu, 9 Jul 2026 19:55:01 -0700 Subject: [PATCH 1/2] Close #43 --- .github/workflows/release.yml | 19 +++++++++++++++++++ docs/release-playbook.md | 23 +++++++++++++++++++++++ scripts/e2e-release-packaging.test.mjs | 25 +++++++++++++++++++++++++ scripts/firefox-webext.js | 15 +++++++++++++++ scripts/sign-firefox-listed.mjs | 12 ++++++------ scripts/sign-firefox-unlisted.mjs | 7 ++----- 6 files changed, 90 insertions(+), 11 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a0c1916..83e0f78 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -80,6 +80,25 @@ jobs: node-version-file: .nvmrc cache: npm + - name: Validate Firefox signing credentials + env: + AMO_JWT_ISSUER: ${{ secrets.AMO_JWT_ISSUER }} + AMO_JWT_SECRET: ${{ secrets.AMO_JWT_SECRET }} + run: | + missing=0 + case "$AMO_JWT_ISSUER" in + (*[![:space:]]*) ;; + (*) echo "::error title=Missing Firefox signing secret::AMO_JWT_ISSUER is not configured for the protected release workflow."; missing=1 ;; + esac + case "$AMO_JWT_SECRET" in + (*[![:space:]]*) ;; + (*) echo "::error title=Missing Firefox signing secret::AMO_JWT_SECRET is not configured for the protected release workflow."; missing=1 ;; + esac + if [ "$missing" -ne 0 ]; then + echo "Configure both protected AMO Actions secrets before rerunning the release." + exit 1 + fi + - name: Install dependencies run: npm ci diff --git a/docs/release-playbook.md b/docs/release-playbook.md index 73bcee1..3915296 100644 --- a/docs/release-playbook.md +++ b/docs/release-playbook.md @@ -27,6 +27,29 @@ - Publish browser store listings and verify installed updates as required by the target browser. - The protected release workflow signs the Firefox unlisted XPI with AMO credentials and uploads it with the other release assets. +### Firefox unlisted-signing credentials + +The protected release workflow requires two GitHub Actions secrets before it +starts the release build: + +- `AMO_JWT_ISSUER` +- `AMO_JWT_SECRET` + +Create the AMO API credential pair in Mozilla Add-ons, then store the values as +repository or protected release-environment secrets with these exact names. +Keep them unavailable to pull-request workflows, do not put them in source, +local release artifacts, or logs, and grant only the permissions required for +Firefox signing. The workflow checks only that each value is present and +non-blank; it never prints either value. + +Rotate both secrets through Mozilla and GitHub when the credential expires or +is suspected to be exposed. After updating them, use +`workflow_dispatch` with `force=true` to rerun the protected release. The +workflow must fail before dependency installation when either secret is absent; +do not bypass signing or publish an unsigned XPI. If the preflight passes but +signing fails, inspect the protected workflow's AMO/web-ext error, correct the +credential or AMO configuration, and rerun the forced release. + ## Firefox Verification Test Plan Use this plan before declaring Firefox support release-ready or bumping the diff --git a/scripts/e2e-release-packaging.test.mjs b/scripts/e2e-release-packaging.test.mjs index 5c8d03e..963440a 100644 --- a/scripts/e2e-release-packaging.test.mjs +++ b/scripts/e2e-release-packaging.test.mjs @@ -23,6 +23,7 @@ const { const { detectManifestVersionChange, } = require('./detect-manifest-version-change.js'); +const { validateAmoCredentials } = require('./firefox-webext.js'); function writeJson(filePath, value) { fs.mkdirSync(path.dirname(filePath), { recursive: true }); @@ -82,6 +83,30 @@ function initGitRepo(repoRoot) { execFileSync('git', ['config', 'user.name', 'Codex'], { cwd: repoRoot }); } +test('e2e: Firefox signing credentials reject missing values', () => { + assert.throws( + () => validateAmoCredentials({}), + /AMO_JWT_ISSUER is required/ + ); +}); + +test('e2e: Firefox signing credentials identify a missing secret', () => { + assert.throws( + () => validateAmoCredentials({ + AMO_JWT_ISSUER: 'issuer', + AMO_JWT_SECRET: ' ', + }), + /AMO_JWT_SECRET is required/ + ); +}); + +test('e2e: Firefox signing credentials accept a complete pair', () => { + assert.doesNotThrow(() => validateAmoCredentials({ + AMO_JWT_ISSUER: 'issuer', + AMO_JWT_SECRET: 'secret', + })); +}); + test('e2e: release version sync fails on source manifest mismatch', () => { const repoRoot = makeRepoFixture(); writeJson(path.join(repoRoot, 'manifest.json'), { diff --git a/scripts/firefox-webext.js b/scripts/firefox-webext.js index 6be18de..a5f93d6 100644 --- a/scripts/firefox-webext.js +++ b/scripts/firefox-webext.js @@ -19,6 +19,20 @@ function ensureFirefoxBuild(repoRoot) { return manifestPath; } +function validateAmoCredentials(env = process.env) { + const apiKey = env.AMO_JWT_ISSUER; + const apiSecret = env.AMO_JWT_SECRET; + + if (typeof apiKey !== 'string' || !/\S/.test(apiKey)) { + throw new Error('AMO_JWT_ISSUER is required to sign a Firefox package.'); + } + if (typeof apiSecret !== 'string' || !/\S/.test(apiSecret)) { + throw new Error('AMO_JWT_SECRET is required to sign a Firefox package.'); + } + + return { apiKey, apiSecret }; +} + function runWebExt(args, options = {}) { const repoRoot = options.repoRoot || path.resolve(__dirname, '..'); const { command, args: prefixArgs } = resolveWebExtCommand(); @@ -49,6 +63,7 @@ function updateReleaseManifest(repoRoot, mutate) { module.exports = { ensureFirefoxBuild, + validateAmoCredentials, runWebExt, updateReleaseManifest, }; diff --git a/scripts/sign-firefox-listed.mjs b/scripts/sign-firefox-listed.mjs index 1715992..ed61282 100644 --- a/scripts/sign-firefox-listed.mjs +++ b/scripts/sign-firefox-listed.mjs @@ -4,18 +4,18 @@ import { createRequire } from 'node:module'; import { fileURLToPath } from 'node:url'; const require = createRequire(import.meta.url); -const { ensureFirefoxBuild, runWebExt } = require('./firefox-webext.js'); +const { + ensureFirefoxBuild, + validateAmoCredentials, + runWebExt, +} = require('./firefox-webext.js'); const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); function main() { ensureFirefoxBuild(repoRoot); - const apiKey = process.env.AMO_JWT_ISSUER; - const apiSecret = process.env.AMO_JWT_SECRET; - if (!apiKey || !apiSecret) { - throw new Error('AMO_JWT_ISSUER and AMO_JWT_SECRET are required to sign the Firefox listed package.'); - } + const { apiKey, apiSecret } = validateAmoCredentials(); const metadataPath = path.join(repoRoot, 'amo', 'metadata', 'listed.json'); if (!fs.existsSync(metadataPath)) { diff --git a/scripts/sign-firefox-unlisted.mjs b/scripts/sign-firefox-unlisted.mjs index 9702fc3..b7407a2 100644 --- a/scripts/sign-firefox-unlisted.mjs +++ b/scripts/sign-firefox-unlisted.mjs @@ -6,6 +6,7 @@ import { fileURLToPath } from 'node:url'; const require = createRequire(import.meta.url); const { ensureFirefoxBuild, + validateAmoCredentials, runWebExt, updateReleaseManifest, } = require('./firefox-webext.js'); @@ -21,11 +22,7 @@ function findSignedArtifact(dir) { function main() { ensureFirefoxBuild(repoRoot); - const apiKey = process.env.AMO_JWT_ISSUER; - const apiSecret = process.env.AMO_JWT_SECRET; - if (!apiKey || !apiSecret) { - throw new Error('AMO_JWT_ISSUER and AMO_JWT_SECRET are required to sign the Firefox unlisted XPI.'); - } + const { apiKey, apiSecret } = validateAmoCredentials(); const artifactsDir = path.join(repoRoot, 'release', 'firefox-unlisted'); fs.mkdirSync(artifactsDir, { recursive: true }); From 0bcfacb7e83881ed4d123c0a6494ffda9c13a5aa Mon Sep 17 00:00:00 2001 From: Kevin Buffardi Date: Fri, 10 Jul 2026 12:40:49 -0700 Subject: [PATCH 2/2] fix(release): expose protected AMO credentials Bind the release artifact job to the protected copilot environment so Firefox signing credentials are available during validation and signing. Document the required environment and secret names.\n\nFixes #45\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/release.yml | 1 + docs/release-playbook.md | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 83e0f78..e0094bd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -67,6 +67,7 @@ jobs: needs: prepare-release if: needs.prepare-release.outputs.should_release == 'true' runs-on: ubuntu-latest + environment: copilot permissions: contents: write diff --git a/docs/release-playbook.md b/docs/release-playbook.md index 3915296..e67d584 100644 --- a/docs/release-playbook.md +++ b/docs/release-playbook.md @@ -36,7 +36,7 @@ starts the release build: - `AMO_JWT_SECRET` Create the AMO API credential pair in Mozilla Add-ons, then store the values as -repository or protected release-environment secrets with these exact names. +secrets in the protected `copilot` GitHub Environment with these exact names. Keep them unavailable to pull-request workflows, do not put them in source, local release artifacts, or logs, and grant only the permissions required for Firefox signing. The workflow checks only that each value is present and