Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
23 changes: 23 additions & 0 deletions docs/release-playbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
25 changes: 25 additions & 0 deletions scripts/e2e-release-packaging.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand Down Expand Up @@ -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'), {
Expand Down
15 changes: 15 additions & 0 deletions scripts/firefox-webext.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -49,6 +63,7 @@ function updateReleaseManifest(repoRoot, mutate) {

module.exports = {
ensureFirefoxBuild,
validateAmoCredentials,
runWebExt,
updateReleaseManifest,
};
12 changes: 6 additions & 6 deletions scripts/sign-firefox-listed.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down
7 changes: 2 additions & 5 deletions scripts/sign-firefox-unlisted.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { fileURLToPath } from 'node:url';
const require = createRequire(import.meta.url);
const {
ensureFirefoxBuild,
validateAmoCredentials,
runWebExt,
updateReleaseManifest,
} = require('./firefox-webext.js');
Expand All @@ -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 });
Expand Down
Loading