ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

将 freehire 浏览器扩展发布到 addons.mozilla.org:从首次人工上架到 CI 自动签名

将 freehire 浏览器扩展发布到 addons.mozilla.org:从首次人工上架到 CI 自动签名

【免费下载链接】freehire

freehire — the open-source search engine for job seekers

项目地址:https://gitcode.com/GitHub_Trending/fr/freehire
点击查看免费下载

本文围绕 freehire 仓库中的 Firefox 发布流程展开:Firefox 构建与 Chrome 构建同源,但上架 addons.mozilla.org(AMO)有独立的一次性人工步骤、源代码包要求、固定的 gecko id 以及基于 JWT 的自动签名链路。读完后你将掌握如何手工完成首次上架、如何准备 reviewer 可复现的 sources ZIP,以及如何用两个仓库密钥让后续每个版本自动提交审核。

配套阅读:Chrome 上架清单 提供完整文案与权限说明;扩展开发指南 说明构建命令与 Firefox 差异。

Firefox 与 Chrome 构建的关系

extension/docs/firefox-addons.md开宗明义:Firefox 构建与 Chrome 构建来自同一份源码,listing 文案、隐私文本和权限申请理由与 Chrome 版本相同,只是减去sidePanel和debugger——Firefox 的 manifest 根本不申请这两个权限。

这一点在 extension/wxt.config.ts 中是可以直接印证的事实:

permissions: [ 'storage', 'tabs', 'scripting', 'activeTab', 'identity', 'downloads', ...(browser === 'firefox' ? [] : ['sidePanel', 'debugger']), ], host_permissions: ['<all_urls>'],

也就是说,Firefox 侧保留storage、tabs、scripting、activeTab、identity、downloads与<all_urls>,而sidePanel、debugger仅在非 Firefox 分支注入。二者各自对应一个产品差异:

  • 没有sidePanel,改用侧边栏。WXT 会把同一个sidepanel入口在 Firefox 下产出为sidebar_action。由于 Firefox 的 sidebar 没有“点击图标自动打开”的设置,extension/entrypoints/background.ts 在点击事件里显式调用sidebarAction.toggle(),这也是 Firefox 要求用户手势才能打开侧栏的原因。
  • 没有debugger,因此没有“附加定制简历”。chrome.debugger在 Firefox 不存在,DOM.setFileInputFiles那条把 PDF 塞进input[type=file]的路径无法实现,面板依据import.meta.env.FIREFOX隐藏该按钮。

其余差异集中在 manifest 的 Firefox 专属字段,后文“gecko id 与登录重定向”一节会详细展开。

第一次上架:必须在浏览器里手工完成

AMO 的发布管道只能给已存在的 add-on 追加版本,无法创建第一个 listing。因此首次上架是一次性人工操作:

  1. 拿到两个文件。从buildworkflow 下载extensionartifact(或最新的extension-v*GitHub Release),也可以本地执行:

    cd extension && npm run zip:firefox

    产物为:

    • freehire-extension-<version>-firefox.zip— 扩展本体;
    • freehire-extension-<version>-sources.zip— 源码包,供 AMO 审核使用。
  2. 进入 Developer Hub(addons.mozilla.org/developers)→Submit a New Add-on→On this site(listed,即公开上架)→ 上传-firefox.zip。

  3. 回答“代码是否压缩/打包”时选 yes,并上传-sources.zip。给 reviewer 的构建说明原文为:

    Node 24, pnpm and npm. From the archive root:cd design-system && pnpm install --frozen-lockfile && cd ../extension && npm ci && npm run build:firefox. The output isextension/.output/firefox-mv2.

  4. 用 Chrome 上架文档 里的文案填写 listing 并提交,AMO 为人工审核。

命令与产物在仓库中的对应关系

npm run zip:firefox并非 WXT 的默认行为,它在 extension/package.json 中被定义为:

"build:firefox": "wxt build -b firefox", "zip:firefox": "WXT_RELEASE=1 wxt zip -b firefox",

WXT_RELEASE=1是关键开关。它让 extension/wxt.config.ts 跳过key字段——发布产物由商店分配 id,硬编码的key会与之冲突;同时它启用了仅商店产物使用的icon_variants(Chrome 侧),Firefox 分支则因browser === 'firefox'而不注入key,因为 Firefox 忽略key,改用gecko.id固定身份。

sources ZIP 里到底放了什么

AMO 会以源码包为准复核打包产物,因此 sources ZIP 的构成是硬性要求,配置在 extension/wxt.config.ts:

zip: { sourcesRoot: '..', includeSources: [ 'extension/**', 'extension/.env.production', 'design-system/**', ], },

三个细节值得注意:

  • sourcesRoot: '..'把压缩包根设为仓库根,因为扩展通过file:../design-system依赖设计系统,源码包必须同时包含design-system/;
  • extension/.env.production被显式列入:隐藏文件默认会被跳过,而缺少它,reviewer 构建出来的扩展会指向 localhost 而不是生产环境;
  • 审核方构建指令里的npm ci会触发postinstall: wxt prepare,生成.wxt/目录,这正是cd extension && npm ci之后能直接npm run build:firefox的原因。

构建前置条件:design-system 必须先装依赖

仓库在多处强调同一个坑:file:/link:链接的包不会被消费方的安装过程带上自己的依赖。因此 reviewer 的指令第一步是cd design-system && pnpm install --frozen-lockfile。这一点在 .github/workflows/extension.yml 中有独立步骤与注释,CI 也因此单独安装 design-system 依赖,否则构建会以MODULE_NOT_FOUND失败。

本地验证 Firefox 产物

首次上架前建议在本地确认侧栏行为。按 extension/AGENTS.md 的说明:执行npm run dev:firefox或npm run build:firefox(输出.output/firefox-mv2),然后打开about:debugging→ This Firefox →Load Temporary Add-on→ 选择该构建的manifest.json。sidepanel入口在 Firefox 下变成sidebar_action,点击工具栏图标即切换显示。

gecko id 与登录重定向:为什么它必须永远不变

extension/docs/firefox-addons.md特别指出:add-on id 在wxt.config.ts中固定为extension@freehire.me,所以上架版登录“已经能正常工作”——它的 redirect hash 已在 hire 的允许列表里。

这句话的机制在源码中非常具体。登录流程见 extension/lib/auth.ts:

const redirectUri = browser.identity.getRedirectURL(); const state = crypto.randomUUID(); const authUrl = `${HIRE_ORIGIN}/api/v1/auth/extension/connect` + `?redirect_uri=${encodeURIComponent(redirectUri)}&state=${encodeURIComponent(state)}`; const responseUrl = await browser.identity.launchWebAuthFlow({ url: authUrl, interactive: true });

Firefox 的identity.getRedirectURL()形如https://<sha1(gecko id)>.extensions.allizom.org/,所以 gecko id 决定了这个 hash。对extension@freehire.me,hash 是13f2644fb08a641dc52d5b7c11faf2fb9775ee26——这个值同时出现在 extension/AGENTS.md 与后端测试 internal/api/handler/extension_connect_test.go 中,并被当作“已允许的 Firefox hash”断言通过。

服务端校验逻辑位于 internal/api/handler/extension_connect.go,要点如下:

校验项规则
scheme必须是https,否则报redirect_uri must be https
port不允许携带端口
host 后缀只接受.chromiumapp.org或.extensions.allizom.org
标签结构后缀前必须是单个label(不能有点号,防止a.<id>.extensions.allizom.org之类绕过)
允许列表该 label 必须出现在EXTENSION_REDIRECT_ALLOWLIST中
空列表直接禁用整个扩展登录流程(extension connect is disabled)

EXTENSION_REDIRECT_ALLOWLIST由 internal/platform/config/config.go 以逗号分隔解析。因此 Firefox 上架的运维动作是明确的:把13f2644fb08a641dc52d5b7c11faf2fb9775ee26加进该环境变量,与 Chrome 的 id 并列。gecko id 一旦改动,hash 随之变化,登录会立刻失败——这与 Chrome 侧“key必须固定”是同一个约束。

顺带一提,minted token 通过 URLfragment回传(#token=…&state=…),而不是 query,避免被日志或Referer带走;扩展侧parseAuthRedirect会校验state后才落盘到storage.local。

后续每个版本:用两个仓库密钥实现自动发布

首次上架完成后,自动发布只需两件事:

  1. 在 AMO 的 Developer Hub →Manage API Keys生成凭据;
  2. 把它们加为仓库密钥:AMO_JWT_ISSUER与AMO_JWT_SECRET。

从此,任何一次推送到main且提升了extension/package.json的version的提交,都会发布一个 GitHub Release,并把同一份构建提交到 addons.mozilla.org(见 .github/workflows/extension.yml)。如果没有配置这两个密钥,该步骤会被跳过。

CI 的完整链路与门控条件

workflow 的触发路径为extension/**、design-system/**与 workflow 文件本身,build作业按顺序执行:安装 design-system 依赖 →npm ci→ lint → 单测 → 类型检查 →npm run zip→npm run zip:firefox→ 上传extension/.output/*.zip(因.output是隐藏目录,需要include-hidden-files: true)。

发布相关步骤有三重门控:

- name: Publish to addons.mozilla.org if: github.event_name == 'push' && steps.gh_release.outputs.created == 'true' && env.AMO_JWT_ISSUER != ''

逐条解释:

  • github.event_name == 'push':PR 构建只产出 artifact,不发布;
  • steps.gh_release.outputs.created == 'true':只有GitHub Release 是新建的才继续。Release 步骤本身是幂等的——若extension-v<version>已存在则输出created=false并跳过,因此“同一版本重复上传”不会被触发(AMO 也会拒绝重复版本号);
  • env.AMO_JWT_ISSUER != '':密钥缺失时这一步为 no-op。

真正调用签名工具的命令如下:

sources_path=$(find .output -maxdepth 1 -name '*-sources.zip') npx --yes web-ext@8 sign \ --source-dir .output/firefox-mv2 \ --channel listed \ --upload-source-code "$sources_path" \ --approval-timeout 0 \ --api-key "$AMO_JWT_ISSUER" \ --api-secret "$AMO_JWT_SECRET"

几个参数值得记住:

  • --channel listed表示提交到公开商店,而不是 self-hosted 的未签名版本;
  • --upload-source-code把 sources ZIP 一并交给审核方,对应首次上架时手工上传的那份包;
  • --approval-timeout 0在上传被接受后立即返回,不等待人工审核。审核需要数天,CI 作业不应阻塞——workflow 注释明确写了这一点。

版本号的唯一来源

版本号来自extension/package.json的version字段(当前为0.1.10),workflow 用node -p "require('./package.json').version"读取并拼成extension-v<version>标签。这意味着:想让 AMO 收到新版本,必须改package.json的version;改代码但不改版本,Release 与 AMO 提交都会因“已存在”而跳过。

上架文案与权限说明的复用边界

extension/docs/firefox-addons.md要求直接复用 Chrome 上架文档 的内容,但要理解“相同”与“不同”的边界:

  • 相同的部分:名称 freehire、≤132 字符的简短描述、Single purpose(“Assist the user with job applications on the page they are currently viewing.”)、详细描述、隐私实践(收集 PII、认证信息、网站内容、匿名使用行为;不收集健康、金融、位置、私人通信、浏览历史、键盘记录),以及商店要求的认证勾选项。
  • 不同的部分:权限理由表要删掉sidePanel与debugger两行,因为 Firefox manifest 不申请它们;其余storage、tabs、scripting、activeTab、identity、host_permissions: <all_urls>的理由逐字可用。

Firefox 侧还有一项 Chrome 没有的声明:data_collection_permissions。它写在 extension/wxt.config.ts 的browser_specific_settings.gecko下:

browser_specific_settings: { gecko: { id: GECKO_ID, strict_min_version: '140.0', data_collection_permissions: { required: ['authenticationInfo', 'websiteContent'], }, }, },

注释说明140是第一个读取data_collection_permissions的版本,而 AMO 对每个新 listing 都要求该声明。声明的两项正好对应扩展的真实数据面:authenticationInfo(登录后保存在storage.local的会话 JWT)与websiteContent(用户提问时读取的页面内容、Autofill 读取的表单字段)。这也解释了为什么 Chrome 文档里“收集四类数据”的措辞在 Firefox 侧需要落到这两项必填声明上。

上架前后自检清单

阶段检查项依据
首次上架前extension/package.json的version未被商店占用AMO 拒绝重复版本
首次上架前-sources.zip同时包含extension/**、extension/.env.production、design-system/**extension/wxt.config.ts
首次上架前本地about:debugging加载.output/firefox-mv2,确认侧栏与登录可用extension/AGENTS.md
首次上架时gecko id 为extension@freehire.me,未改动extension/wxt.config.ts
首次上架后服务端EXTENSION_REDIRECT_ALLOWLIST含13f2644fb08a641dc52d5b7c11faf2fb9775ee26internal/identity/auth/AGENTS.md
自动化前仓库已配置AMO_JWT_ISSUER、AMO_JWT_SECRET.github/workflows/extension.yml
每次发版version已提升,Release 标签为extension-v<version>workflow 版本读取步骤
发版失败重试先删除已存在的 Release 与标签,再重跑Release 步骤的幂等判断

最后一点值得强调:Firefox 的自动发布是“只追加版本”,不会帮你在 AMO 创建 listing。首次那一步永远留在浏览器里,自动化只是把它之后的手工上传动作去掉了,审核等待并不会消失——同样的逻辑在 Chrome 侧也成立,那里<all_urls>注定触发人工深度审核。理解这条边界,才能正确规划 freehire 扩展在两条商店渠道上的发版节奏。

【免费下载链接】freehire

freehire — the open-source search engine for job seekers

项目地址:https://gitcode.com/GitHub_Trending/fr/freehire
点击查看免费下载
上一篇:远程工作资源大全:awesome-remote-job 精选清单实战指南
下一篇:dplyr 列重命名完全指南:rename() 与 rename_with() 的语法、源码实现与错误校验机制

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进