├── .gitattributes ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── 0.rfc.yml │ ├── 1.bug.yml │ └── config.yml ├── SECURITY.md └── workflows │ ├── deploy-staging.yml │ ├── deploy-v.yml │ └── pr-test.yml ├── .gitignore ├── .gitmodules ├── .npmrc ├── .prettierrc.mjs ├── .vscode ├── extensions.json └── settings.json ├── LICENSE ├── README.md ├── action.yml ├── keys.schema.json ├── keys.template.json ├── package.json ├── packages ├── bpp │ ├── jest.config.mjs │ ├── package.json │ ├── src │ │ └── main.ts │ ├── tests │ │ ├── __snapshots__ │ │ │ └── main.test.ts.snap │ │ └── main.test.ts │ └── tsconfig.json └── esbuild-plugins │ ├── inline-fflate.mjs │ └── package.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml └── turbo.json /.gitattributes: -------------------------------------------------------------------------------- 1 | dist/** -diff linguist-generated=true -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @plasmohq/engineers 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/0.rfc.yml: -------------------------------------------------------------------------------- 1 | name: ⚡ Request for Comments 2 | description: File an RFC for Feature Request/Enhancement/Refactor 3 | title: "[RFC] " 4 | labels: ["enhancement"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | **Thank you for taking the time to fill out this RFC!** 🥳 10 | 11 | - type: textarea 12 | id: description 13 | attributes: 14 | label: How do you envision this feature/change to look/work like? 15 | description: Please be as detailed as possible, providing any relevant context. 16 | placeholder: The action should read safari config and spit out fire 17 | validations: 18 | required: true 19 | 20 | - type: textarea 21 | id: purpose 22 | attributes: 23 | label: What is the purpose of this change/feature? Why? 24 | description: Please provide a simple summary/abstraction. 25 | placeholder: | 26 | The current image is not versatile enough, i.e it is too small. 27 | validations: 28 | required: true 29 | 30 | - type: textarea 31 | id: examples 32 | attributes: 33 | label: (OPTIONAL) Example implementations 34 | description: If you have any examples of how this feature/change works, please list them here. 35 | validations: 36 | required: false 37 | 38 | - type: checkboxes 39 | id: terms 40 | attributes: 41 | label: Code of Conduct 42 | description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/PlasmoHQ/plasmo/blob/main/.github/CONTRIBUTING.md). 43 | options: 44 | - label: I agree to follow this project's Code of Conduct 45 | required: true 46 | - label: I checked the [current issues](https://github.com/PlasmoHQ/bpp/issues?q=is%3Aopen+is%3Aissue+label%3Aenhancement+) for duplicate problems. 47 | required: true 48 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/1.bug.yml: -------------------------------------------------------------------------------- 1 | name: 🐛 Bug Report 2 | description: File a bug report 3 | title: "[BUG] " 4 | labels: ["bug", "triage"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | **Thank you for taking the time to fill out this bug report!** 🥳 10 | 11 | - type: textarea 12 | id: what-happened 13 | attributes: 14 | label: What happened? 15 | description: Also tell us, what did you expect to happen? 16 | placeholder: Tell us what you see! 17 | value: "A bug happened!" 18 | validations: 19 | required: true 20 | 21 | - type: dropdown 22 | id: browsers 23 | attributes: 24 | label: Which browsers are you seeing the problem on? 25 | multiple: true 26 | options: 27 | - Chrome 28 | - Microsoft Edge 29 | - Opera 30 | - Safari 31 | - Firefox 32 | 33 | - type: textarea 34 | id: logs 35 | attributes: 36 | label: Relevant log output 37 | description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. 38 | render: Shell 39 | 40 | - type: checkboxes 41 | id: terms 42 | attributes: 43 | label: Code of Conduct 44 | description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/PlasmoHQ/plasmo/blob/main/.github/CONTRIBUTING.md). 45 | options: 46 | - label: I agree to follow this project's Code of Conduct 47 | required: true 48 | - label: I checked the [current issues](https://github.com/PlasmoHQ/bpp/issues?q=is%3Aopen+is%3Aissue+label%3Abug) for duplicate problems. 49 | required: true 50 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Join our Discord server 4 | url: https://www.plasmo.com/s/d 5 | about: Ask questions and discuss with other community members 6 | -------------------------------------------------------------------------------- /.github/SECURITY.md: -------------------------------------------------------------------------------- 1 | Contact: security@plasmo.com 2 | Expires: 2100-01-01T00:00:00.000Z 3 | Acknowledgments: https://www.plasmo.com/security/hall-of-fame 4 | -------------------------------------------------------------------------------- /.github/workflows/deploy-staging.yml: -------------------------------------------------------------------------------- 1 | name: Staging deployment 2 | # rebuild any PRs and main branch changes 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - main 8 | pull_request: 9 | branches: 10 | - main 11 | 12 | jobs: 13 | build: # make sure build/ci work properly 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | with: 18 | submodules: "recursive" 19 | - name: Cache pnpm modules 20 | uses: actions/cache@v4 21 | with: 22 | path: ~/.pnpm-store 23 | key: ${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }} 24 | restore-keys: | 25 | ${{ runner.os }}- 26 | - uses: pnpm/action-setup@v4 27 | with: 28 | version: 9.12.3 29 | run_install: true 30 | - name: Use Node.js 20.x 31 | uses: actions/setup-node@v4 32 | with: 33 | node-version: 20.x 34 | cache: "pnpm" 35 | - run: pnpm run package 36 | 37 | - run: cp -r packages/bpp/dist dist 38 | 39 | - run: cp action.yml dist/ 40 | - run: cp README.md dist/ 41 | - run: cp keys.schema.json dist/ 42 | 43 | - name: Deploy to staging 44 | env: 45 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 46 | run: | 47 | git remote set-url origin https://git:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git 48 | pnpm dlx gh-pages -d dist -b staging -u "Plasmo G.A.T Deployer " 49 | 50 | test: # make sure the action works on a clean machine without building 51 | runs-on: ubuntu-latest 52 | needs: build 53 | steps: 54 | - uses: actions/checkout@v4 55 | with: 56 | ref: staging 57 | - uses: ./ 58 | env: 59 | NODE_ENV: "test" 60 | with: 61 | artifact: build/artifact.zip 62 | keys: ${{ secrets.BPP_KEYS }} 63 | -------------------------------------------------------------------------------- /.github/workflows/deploy-v.yml: -------------------------------------------------------------------------------- 1 | # Deploy staging to the major version branch 2 | name: Major version rolling deployment 3 | 4 | # Bump this version accordingly as the action matures 5 | env: 6 | VERSION: v3 7 | 8 | on: 9 | workflow_dispatch: 10 | 11 | jobs: 12 | deploy: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | with: 17 | ref: staging 18 | submodules: "recursive" 19 | 20 | - name: Configure git information 21 | run: | 22 | git config --global user.name 'Plasmo G.A.T Deployer' 23 | git config --global user.email 'support@plasmo.com' 24 | 25 | - run: git checkout -b ${{ env.VERSION }} 26 | - name: Deploy ${{ env.VERSION }} 27 | env: 28 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 29 | run: | 30 | git remote set-url origin https://git:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git 31 | git push -fu origin ${{ env.VERSION }} 32 | 33 | check-deployment: 34 | needs: deploy 35 | runs-on: ubuntu-latest 36 | steps: 37 | - uses: actions/checkout@v4 38 | with: 39 | ref: ${{ env.VERSION }} 40 | 41 | - name: Compare the expected and actual deployed directories 42 | run: | 43 | if [ "$(git diff staging --ignore-space-at-eol ./ | wc -l)" -gt "0" ]; then 44 | echo "Detected uncommitted changes after build. See status below:" 45 | git diff 46 | exit 1 47 | fi 48 | id: diff 49 | 50 | # If index.js was different than expected, upload the expected version as an artifact 51 | - uses: actions/upload-artifact@v4 52 | if: ${{ failure() && steps.diff.conclusion == 'failure' }} 53 | with: 54 | name: dist 55 | path: ./ 56 | -------------------------------------------------------------------------------- /.github/workflows/pr-test.yml: -------------------------------------------------------------------------------- 1 | name: PR testing 2 | # rebuild any PRs and main branch changes 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | build: # make sure build/ci work properly 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | with: 14 | submodules: "recursive" 15 | - name: Cache pnpm modules 16 | uses: actions/cache@v4 17 | with: 18 | path: ~/.pnpm-store 19 | key: ${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }} 20 | restore-keys: | 21 | ${{ runner.os }}- 22 | - uses: pnpm/action-setup@v4 23 | with: 24 | version: 9.12.3 25 | run_install: true 26 | - name: Use Node.js 20.x 27 | uses: actions/setup-node@v4 28 | with: 29 | node-version: 20.x 30 | cache: "pnpm" 31 | - run: pnpm run package 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependency directory 2 | node_modules 3 | 4 | # Rest pulled from https://github.com/github/gitignore/blob/master/Node.gitignore 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # Diagnostic reports (https://nodejs.org/api/report.html) 14 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 15 | 16 | # Runtime data 17 | pids 18 | *.pid 19 | *.seed 20 | *.pid.lock 21 | 22 | # Directory for instrumented libs generated by jscoverage/JSCover 23 | lib-cov 24 | 25 | # Coverage directory used by tools like istanbul 26 | coverage 27 | *.lcov 28 | 29 | # nyc test coverage 30 | .nyc_output 31 | 32 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 33 | .grunt 34 | 35 | # Bower dependency directory (https://bower.io/) 36 | bower_components 37 | 38 | # node-waf configuration 39 | .lock-wscript 40 | 41 | # Compiled binary addons (https://nodejs.org/api/addons.html) 42 | build/Release 43 | 44 | # Dependency directories 45 | jspm_packages/ 46 | 47 | # TypeScript v1 declaration files 48 | typings/ 49 | 50 | # TypeScript cache 51 | *.tsbuildinfo 52 | 53 | # Optional npm cache directory 54 | .npm 55 | 56 | # Optional eslint cache 57 | .eslintcache 58 | 59 | # Optional REPL history 60 | .node_repl_history 61 | 62 | # Output of 'npm pack' 63 | *.tgz 64 | 65 | # Yarn Integrity file 66 | .yarn-integrity 67 | 68 | # dotenv environment variables file 69 | .env 70 | .env.test 71 | 72 | # parcel-bundler cache (https://parceljs.org/) 73 | .cache 74 | 75 | # next.js build output 76 | .next 77 | 78 | # nuxt.js build output 79 | .nuxt 80 | 81 | # vuepress build output 82 | .vuepress/dist 83 | 84 | # Serverless directories 85 | .serverless/ 86 | 87 | # FuseBox cache 88 | .fusebox/ 89 | 90 | # DynamoDB Local files 91 | .dynamodb/ 92 | 93 | # OS metadata 94 | .DS_Store 95 | Thumbs.db 96 | 97 | # Ignore built ts files 98 | tests/runner/* 99 | lib/**/* 100 | 101 | dist/ 102 | 103 | .turbo -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "packages/bms"] 2 | path = packages/bms 3 | url = git@github.com:PlasmoHQ/bms.git 4 | [submodule "packages/edge-addons-api"] 5 | path = packages/edge-addons-api 6 | url = git@github.com:PlasmoHQ/edge-addons-api.git 7 | [submodule "packages/chrome-webstore-api"] 8 | path = packages/chrome-webstore-api 9 | url = git@github.com:PlasmoHQ/chrome-webstore-api.git 10 | [submodule "packages/mozilla-addons-api"] 11 | path = packages/mozilla-addons-api 12 | url = git@github.com:PlasmoHQ/mozilla-addons-api.git 13 | [submodule "packages/safari-webstore-api"] 14 | path = packages/safari-webstore-api 15 | url = git@github.com:PlasmoHQ/safari-webstore-api.git 16 | [submodule "packages/config"] 17 | path = packages/config 18 | url = https://github.com/PlasmoHQ/plasmo-config.git 19 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | save-workspace-protocol = true 2 | prefer-workspace-packages = true 3 | save-exact = true 4 | link-workspace-packages = true 5 | strict-peer-dependencies = false -------------------------------------------------------------------------------- /.prettierrc.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {import('prettier').Options} 3 | */ 4 | export default { 5 | printWidth: 80, 6 | tabWidth: 2, 7 | useTabs: false, 8 | semi: false, 9 | singleQuote: false, 10 | trailingComma: "none", 11 | bracketSpacing: true, 12 | bracketSameLine: true, 13 | plugins: ["@ianvs/prettier-plugin-sort-imports"], 14 | importOrder: [ 15 | "", // Node.js built-in modules 16 | "", // Imports not matched by other special words or groups. 17 | "", // Empty line 18 | "^@plasmo/(.*)$", 19 | "", 20 | "^@plasmohq/(.*)$", 21 | "", 22 | "^@plasmo-static-common/(.*)$", 23 | "", 24 | "^~(.*)$", 25 | "", 26 | "^[./]", 27 | "", 28 | "__plasmo_import_module__", 29 | "__plasmo_mount_content_script__" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["esbenp.prettier-vscode"] 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "esbenp.prettier-vscode", 3 | "prettier.configPath": ".prettierrc.cjs", 4 | "files.exclude": { 5 | "**/.git": true, 6 | "**/.svn": true, 7 | "**/.hg": true, 8 | "**/CVS": true, 9 | "**/.DS_Store": true, 10 | "**/Thumbs.db": true, 11 | "**/node_modules": true, 12 | "**/.turbo": true, 13 | "**/.next": true, 14 | "**/.swc": true, 15 | "**/*.log": true 16 | }, 17 | "[svg]": { 18 | "editor.defaultFormatter": "jock.svg" 19 | }, 20 | "workbench.colorCustomizations": { 21 | "titleBar.activeForeground": "#ffffff", 22 | "titleBar.activeBackground": "#0b3f87" // Change this color! 23 | }, 24 | "git.detectSubmodulesLimit": 99, 25 | "typescript.tsdk": "node_modules\\typescript\\lib" 26 | } 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2022 Plasmo Corp. (https://www.plasmo.com) and contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | plasmo logo 4 | 5 |

6 | 7 |

8 | 9 | See License 10 | 11 | 12 | Follow PlasmoHQ on Twitter 13 | 14 | 15 | Watch our Live DEMO every Friday 16 | 17 | 18 | Join our Discord for support and chat about our projects 19 | 20 | 21 | typescript-action status 22 | 23 |

24 | 25 | # Browser Platform Publisher 26 | 27 | A GitHub action from [Plasmo](https://www.plasmo.com/) to publish your browser extension to every web store/add-ons marketplace. This action and its dependencies are open-sourced under the MIT license. The core modules are: 28 | 29 | - [Browser Market Submit](https://github.com/PlasmoHQ/bms) 30 | - [Chrome Webstore API](https://github.com/PlasmoHQ/chrome-webstore-api) 31 | - [Mozilla Addons API](https://github.com/PlasmoHQ/mozilla-addons-api) 32 | - [Edge Addons API](https://github.com/PlasmoHQ/edge-addons-api) 33 | 34 | ## Usage 35 | 36 | In order to use this action, you will need to create a json file that contains the keys and optional configuration for each browser you wish to publish to. 37 | 38 | To help you create it, we have provided a [JSON schema](https://json-schema.org/) that can be used with editors that support it, such as Visual Studio Code. This schema will provide intellisense and validation to ensure that your configuration is correct. 39 | 40 | ```json 41 | { 42 | "$schema": "https://github.com/PlasmoHQ/bpp/raw/main/keys.schema.json" 43 | } 44 | ``` 45 | 46 | > **NOTE**: You should only specify the browsers you wish to publish to. If there are any invalid configuration, the action will fail! I.e, no empty key allowed such as `"chrome": {}`. 47 | 48 | Each browser option is made of the following: 49 | 50 | * Mandatory browser specific tokens (see [token guide](https://github.com/PlasmoHQ/bms/blob/main/tokens.md)) 51 | 52 | * Optional parameters: 53 | * `zip`: The zip file containing the extension. The manifest.json file must be in the root of the zip file. 54 | 55 | `{version}` can be used in the name and will be replaced by the version from your versionFile. 56 | 57 | * `file`: An alias for the zip property. 58 | 59 | * `verbose`: Enable verbose logging for the specific browser. 60 | 61 | * `versionFile`: Relative path to a json file which has a version field. Defaults to package.json 62 | 63 | * `sourceZip`: The zip file containing the source code for Firefox submissions. 64 | 65 | * `source` and `sourceFile`: Aliases for the `sourceZip` property. 66 | 67 | * `notes`: [Edge Only] Provide notes for certification to the Edge Add-ons reviewers. 68 | 69 | The final json might look like this: 70 | 71 | ```json 72 | { 73 | "$schema": "https://github.com/PlasmoHQ/bpp/raw/main/keys.schema.json", 74 | "chrome": { 75 | "zip": "chromium_addon_{version}.zip", 76 | "clientId": "1280623", 77 | "clientSecret": "1!9us4", 78 | "refreshToken": "7&as$a89", 79 | "extId": "akszypg" 80 | }, 81 | "firefox": { 82 | "file": "firefox_addon.xpi", 83 | "sourceFile": "source.zip", 84 | "extId": "akszypg", 85 | "apiKey": "ab214c4d", 86 | "apiSecret": "e%f253^gh" 87 | }, 88 | "edge": { 89 | "zip": "chromium_addon.zip", 90 | "clientId": "aaaaaaa-aaaa-bbbb-cccc-dddddddddddd", 91 | "apiKey": "ab#c4de6fg", 92 | "productId": "aaaaaaa-aaaa-bbbb-cccc-dddddddddddd", 93 | "notes": "This is a test submission" 94 | } 95 | } 96 | ``` 97 | 98 | Once you got your json file, you will need to copy its content into a GitHub secret. You can do this by following the [instructions on creating encrypted secrets for a repository](https://docs.github.com/en/actions/security-guides/encrypted-secrets#creating-encrypted-secrets-for-a-repository). In this example, we will use the secret name `BPP_KEYS`. 99 | 100 | Then, the action can be used as follows: 101 | 102 | ```yaml 103 | steps: 104 | - name: Browser Platform Publish 105 | uses: PlasmoHQ/bpp@v3 106 | with: 107 | keys: ${{ secrets.BPP_KEYS }} 108 | ``` 109 | 110 | ## Action Input Parameters (`with`) 111 | The following parameters can be used to override the configuration in the keys file. 112 | Specifying options here will **override** those in the keys file. 113 | ```yaml 114 | keys: 115 | required: true 116 | description: "A JSON string containing the keys to be used for the submission process. (This should be a secret)" 117 | artifact: 118 | alias: [zip, file] 119 | required: false 120 | description: "The extension zip artifact to be published on all stores." 121 | opera-file/chrome-file/firefox-file/edge-file: 122 | required: false 123 | description: "The file to be published to a specific store." 124 | source: 125 | alias: [source-zip, source-file] 126 | required: false 127 | description: "The extension source zip artifact for Firefox submissions." 128 | notes: 129 | alias: [edge-notes] 130 | required: false 131 | description: "[Edge only] A release note cataloging changes." 132 | verbose: 133 | required: false 134 | description: "Print out more verbose logging." 135 | version-file: 136 | required: false 137 | description: "The path to a json file with a version field, default to package.json." 138 | ``` 139 | 140 | ### Custom input parameters example 141 | ```yaml 142 | steps: 143 | - name: Browser Platform Publish 144 | uses: PlasmoHQ/bpp@v3 145 | with: 146 | keys: ${{ secrets.BPP_KEYS }} 147 | chrome-file: "chrome/myext_chromium_${{ env.EXT_VERSION }}.zip" 148 | edge-file: "edge/myext_edge_${{ env.EXT_VERSION }}.zip" 149 | edge-notes: "This is a test submission" 150 | version-file: "src/manifest.json" 151 | verbose: true 152 | ``` 153 | 154 | # Support 155 | 156 | Join the [Discord channel](https://www.plasmo.com/s/d)! 157 | 158 | # License 159 | 160 | [MIT](./LICENSE) ⭐ [Plasmo Corp.](https://plasmo.com) 161 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: "Browser Platform Publisher" 2 | description: "Publish your browser extension anywhere using a single action." 3 | author: "Plasmo Corp." 4 | branding: 5 | icon: upload-cloud 6 | color: purple 7 | inputs: 8 | keys: 9 | required: true 10 | description: "A JSON string containing the keys to be used for the submission process. (This should be a secret)" 11 | artifact: 12 | required: false 13 | description: "The extension zip artifact to be published." 14 | zip: 15 | required: false 16 | description: "This is an alias to artifact argument." 17 | file: 18 | required: false 19 | description: "This is an alias to artifact argument." 20 | chrome-file: 21 | required: false 22 | description: "The path to the Chrome extension zip file." 23 | firefox-file: 24 | required: false 25 | description: "The path to the Firefox extension zip file." 26 | edge-file: 27 | required: false 28 | description: "The path to the Edge extension zip file." 29 | opera-file: 30 | required: false 31 | description: "The path to the Opera extension zip file." 32 | notes: 33 | required: false 34 | description: "A release note cataloging changes. (Edge only)" 35 | source: 36 | required: false 37 | description: "The extension source zip file." 38 | source-zip: 39 | required: false 40 | description: "This is an alias to source argument." 41 | source-file: 42 | required: false 43 | description: "This is an alias to source argument." 44 | edge-notes: 45 | required: false 46 | description: "This is an alias to notes argument." 47 | verbose: 48 | required: false 49 | description: "Print out more verbose logging." 50 | version-file: 51 | required: false 52 | description: "The path to a json file with a version field, default to package.json." 53 | runs: 54 | using: "node20" 55 | main: "index.js" 56 | -------------------------------------------------------------------------------- /keys.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-06/schema#", 3 | "$id": "https://plasmo.com/bpp/keys.schema.json", 4 | "title": "Browser Platform Publisher Keys v3", 5 | "description": "Deployment Keys for the Plasmo's BPP Action.", 6 | "$ref": "#/definitions/Root", 7 | "definitions": { 8 | "Root": { 9 | "type": "object", 10 | "properties": { 11 | "chrome": { 12 | "allOf": [ 13 | { "$ref": "#/definitions/Common" }, 14 | { "$ref": "#/definitions/Chrome" } 15 | ] 16 | }, 17 | "firefox": { 18 | "allOf": [ 19 | { "$ref": "#/definitions/Common" }, 20 | { "$ref": "#/definitions/Firefox" } 21 | ] 22 | }, 23 | "edge": { 24 | "allOf": [ 25 | { "$ref": "#/definitions/Common" }, 26 | { "$ref": "#/definitions/Edge" } 27 | ] 28 | }, 29 | "itero": { 30 | "allOf": [ 31 | { "$ref": "#/definitions/Common" }, 32 | { "$ref": "#/definitions/Itero" } 33 | ] 34 | } 35 | } 36 | }, 37 | "Common": { 38 | "description": "Common properties", 39 | "type": "object", 40 | "properties": { 41 | "zip": { 42 | "type": "string", 43 | "description": "The zip file containing the extension. `{version}` can be used in the name and will be replaced by the version in the manifest.json file." 44 | }, 45 | "file": { 46 | "type": "string", 47 | "description": "Alias to zip" 48 | }, 49 | "notes": { 50 | "type": "string", 51 | "description": "Provide notes for certification to the Edge Add-ons reviewers" 52 | }, 53 | "verbose": { 54 | "type": "boolean", 55 | "description": "Enable verbose logging" 56 | } 57 | } 58 | }, 59 | "Chrome": { 60 | "description": "For refreshToken and clientId, follow: https://github.com/PlasmoHQ/chrome-webstore-api/blob/main/token.md", 61 | "type": "object", 62 | "properties": { 63 | "extId": { 64 | "type": "string", 65 | "description": "Get it from https://chrome.google.com/webstore/detail/EXT_ID" 66 | }, 67 | "refreshToken": { 68 | "type": "string", 69 | "description": "See: https://github.com/PlasmoHQ/chrome-webstore-api/blob/main/token.md" 70 | }, 71 | "clientId": { 72 | "type": "string", 73 | "description": "See: https://github.com/PlasmoHQ/chrome-webstore-api/blob/main/token.md" 74 | }, 75 | "clientSecret": { 76 | "type": "string", 77 | "description": "See: https://github.com/PlasmoHQ/chrome-webstore-api/blob/main/token.md" 78 | }, 79 | "target": { 80 | "type": "string", 81 | "description": "The target audience to publish to. Defaults to 'default'", 82 | "enum": ["default", "trustedTesters"], 83 | "default": "default" 84 | }, 85 | "uploadOnly": { 86 | "type": "boolean", 87 | "description": "Only upload the extension, do not publish it. Defaults to false", 88 | "default": false 89 | } 90 | }, 91 | "required": ["extId", "refreshToken", "clientId", "clientSecret"] 92 | }, 93 | "Firefox": { 94 | "type": "object", 95 | "description": "For API Key and API Secret, visit: https://addons.mozilla.org/en-US/developers/addon/api/key/", 96 | "properties": { 97 | "apiKey": { 98 | "type": "string", 99 | "description": "The JWT issuer, from https://addons.mozilla.org/en-US/developers/addon/api/key/" 100 | }, 101 | "apiSecret": { 102 | "type": "string", 103 | "description": "The JWT secret, from https://addons.mozilla.org/en-US/developers/addon/api/key/" 104 | }, 105 | "extId": { 106 | "type": "string", 107 | "description": "This is the extension UUID from https://addons.mozilla.org/en-US/developers/addon/EXT_ID/edit under Technical Details, or the URL slug. If it is embedded in your manifest under gecko.id, omit this property." 108 | }, 109 | "license": { 110 | "type": "string", 111 | "description": "The license for your extension. Defaults to 'all-right-reserved'. See: https://addons-server.readthedocs.io/en/latest/topics/api/licenses.html" 112 | }, 113 | "channel": { 114 | "type": "string", 115 | "description": "The channel to publish to. Defaults to 'listed'.", 116 | "enum": ["listed", "unlisted"] 117 | } 118 | }, 119 | "required": ["apiKey", "apiSecret"] 120 | }, 121 | "Edge": { 122 | "type": "object", 123 | "properties": { 124 | "productId": { 125 | "type": "string", 126 | "description": "Create an Edge add-on and get it from the dashboard: https://partner.microsoft.com/en-us/dashboard/microsoftedge/{product-id}/package/dashboard" 127 | }, 128 | "clientId": { 129 | "type": "string", 130 | "description": "You can find your client ID by visiting the Microsoft Partner Center: https://partner.microsoft.com/en-us/dashboard/microsoftedge/publishapi" 131 | }, 132 | "apiKey": { 133 | "type": "string", 134 | "description": "You can find your api key by visiting the Microsoft Partner Center: https://partner.microsoft.com/en-us/dashboard/microsoftedge/publishapi" 135 | }, 136 | "uploadOnly": { 137 | "type": "boolean", 138 | "description": "Only upload the extension, do not publish it. Defaults to false", 139 | "default": false 140 | } 141 | }, 142 | "required": ["productId", "clientId", "clientSecret", "accessTokenUrl"] 143 | }, 144 | "Itero": { 145 | "type": "object", 146 | "description": "NOTE: Itero TestBed requires a paid subscription. See: https://itero.plasmo.com", 147 | "properties": { 148 | "privateKey": { 149 | "type": "string" 150 | }, 151 | "token": { 152 | "type": "string" 153 | }, 154 | "entityId": { 155 | "type": "string" 156 | }, 157 | "extensionId": { 158 | "type": "string" 159 | } 160 | }, 161 | "required": ["privateKey", "token", "extensionId", "entityId"] 162 | }, 163 | "Opera": { 164 | "type": "object", 165 | "description": "NOTE: Opera add-ons reviewer require inspecting your extension's source code.", 166 | "properties": { 167 | "packageId": { 168 | "type": "string", 169 | "description": "Get it from https://addons.opera.com/developer/package/PACKAGE_ID" 170 | }, 171 | "changelog": { 172 | "type": "string", 173 | "description": "Provide changelog for the Opera users" 174 | } 175 | }, 176 | "required": ["packageId"] 177 | } 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /keys.template.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://github.com/PlasmoHQ/bpp/raw/main/keys.schema.json", 3 | "chrome": { 4 | "clientId": "123", 5 | "clientSecret": "123", 6 | "refreshToken": "789", 7 | "extId": "abcd", 8 | "uploadOnly": true 9 | }, 10 | "firefox": { 11 | "extId": "abcd", 12 | "apiKey": "abcd", 13 | "apiSecret": "efgh" 14 | }, 15 | "edge": { 16 | "clientId": "aaaaaaa-aaaa-bbbb-cccc-dddddddddddd", 17 | "apiKey": "abcdefg", 18 | "productId": "aaaaaaa-aaaa-bbbb-cccc-dddddddddddd" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "p1asm0-bpp", 3 | "private": true, 4 | "workspaces": [ 5 | "packages/*" 6 | ], 7 | "scripts": { 8 | "build": "turbo run build --filter=\"!@plasmohq/safari-*\"", 9 | "package": "turbo run package --filter=@plasmo/bpp", 10 | "pp": "pnpm --filter=\"!@plasmohq/safari-*\" publish", 11 | "vp": "pnpm --filter \"./packages/**\" --parallel -r exec pnpm version --commit-hooks false --git-tag-version false --workspaces-update" 12 | }, 13 | "devDependencies": { 14 | "@ianvs/prettier-plugin-sort-imports": "4.4.0", 15 | "esbuild": "0.24.0", 16 | "prettier": "3.3.3", 17 | "turbo": "2.3.3", 18 | "typescript": "5.6.3" 19 | }, 20 | "packageManager": "pnpm@9.12.3" 21 | } 22 | -------------------------------------------------------------------------------- /packages/bpp/jest.config.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {import('@jest/types').Config.InitialOptions} 3 | */ 4 | export default { 5 | clearMocks: true, 6 | moduleFileExtensions: ["js", "ts"], 7 | testMatch: ["**/*.test.ts"], 8 | transform: { 9 | "^.+\\.ts$": "ts-jest" 10 | }, 11 | verbose: true 12 | } 13 | -------------------------------------------------------------------------------- /packages/bpp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@plasmo/bpp", 3 | "version": "3.8.0", 4 | "private": true, 5 | "description": "Browser Platform Publisher", 6 | "module": "lib/main.js", 7 | "scripts": { 8 | "dev": "run-p dev:*", 9 | "dev:compile": "esbuild src/main.ts --sourcemap --bundle --watch --platform=node --outfile=dist/index.js", 10 | "dev:test": "jest --watch", 11 | "format": "prettier --write **/*.ts", 12 | "format-check": "prettier --check **/*.ts", 13 | "build": "esbuild src/main.ts --platform=node --minify --bundle --outfile=dist/index.js", 14 | "test": "jest", 15 | "test:ss": "jest --updateSnapshot", 16 | "package": "run-s format build test" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/plasmohq/bpp.git" 21 | }, 22 | "keywords": [ 23 | "browser-extensions", 24 | "chrome", 25 | "firefox", 26 | "edge", 27 | "safari", 28 | "actions" 29 | ], 30 | "author": "Plasmo Corp. ", 31 | "license": "MIT", 32 | "dependencies": { 33 | "@actions/core": "1.11.1", 34 | "@plasmohq/bms": "workspace:*" 35 | }, 36 | "devDependencies": { 37 | "@jest/globals": "29.7.0", 38 | "@plasmo/config": "workspace:*", 39 | "@plasmohq/rps": "1.8.7", 40 | "@types/node": "22.9.0", 41 | "esbuild": "0.24.0", 42 | "jest": "29.7.0", 43 | "js-yaml": "4.1.0", 44 | "ts-jest": "29.2.5", 45 | "typescript": "5.6.3" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /packages/bpp/src/main.ts: -------------------------------------------------------------------------------- 1 | import { debug, getInput, info, setFailed, warning } from "@actions/core" 2 | import { 3 | BrowserName, 4 | type ChromeOptions, 5 | type CommonOptions, 6 | type EdgeOptions, 7 | type FirefoxOptions, 8 | type IteroOptions, 9 | type OperaOptions, 10 | submitChrome, 11 | submitEdge, 12 | submitFirefox, 13 | submitItero, 14 | supportedBrowserSet 15 | } from "@plasmohq/bms" 16 | 17 | type Keys = { 18 | [BrowserName.Chrome]: ChromeOptions 19 | [BrowserName.Firefox]: FirefoxOptions 20 | [BrowserName.Opera]: OperaOptions 21 | [BrowserName.Edge]: EdgeOptions 22 | [BrowserName.Itero]: IteroOptions 23 | } 24 | 25 | const tag = (prefix: string) => `${prefix.padEnd(9)} |` 26 | 27 | const getBundleFiles = (opt: CommonOptions) => opt.zip || opt.file 28 | 29 | const hasBundleFile = (opt: CommonOptions) => !!getBundleFiles(opt) 30 | 31 | async function run(): Promise { 32 | try { 33 | info(`🟣 Plasmo Browser Platform Publish v3`) 34 | 35 | // All the keys necessary to deploy the extension 36 | const keys: Keys = JSON.parse(getInput("keys", { required: true })) 37 | // Path to the zip file to be deployed 38 | const artifact = getInput("file") || getInput("zip") || getInput("artifact") 39 | // Path to the source zip file for firefox submissions 40 | const source = getInput("source") || getInput("source-file") || getInput("source-zip") 41 | 42 | const versionFile = getInput("version-file") 43 | 44 | const edgeNotes = getInput("notes") || getInput("edge-notes") 45 | 46 | const verbose = !!getInput("verbose") 47 | 48 | if (verbose) { 49 | // All internal logging goes to info 50 | console.log = info 51 | } 52 | 53 | const browserEntries = Object.keys(keys).filter((browser) => 54 | supportedBrowserSet.has(browser as BrowserName) 55 | ) as BrowserName[] 56 | 57 | if (browserEntries.length === 0) { 58 | throw new Error("No supported browser found") 59 | } 60 | 61 | for (const browser of browserEntries) { 62 | const fromInput = getInput(`${browser}-file`) 63 | const fromKeys = getBundleFiles(keys[browser]) 64 | if (fromInput) { 65 | keys[browser].zip = fromInput 66 | } else if (fromKeys) { 67 | keys[browser].zip = fromKeys 68 | } else if (artifact) { 69 | keys[browser].zip = artifact 70 | } else { 71 | warning( 72 | `${tag("🟡 SKIP")} No artifact available to submit for ${browser}` 73 | ) 74 | } 75 | 76 | // override keys value if verbose/versionFile action input is set 77 | if (verbose) { 78 | keys[browser].verbose = verbose 79 | } 80 | 81 | if (versionFile) { 82 | keys[browser].versionFile = versionFile 83 | } 84 | } 85 | 86 | const hasAtLeastOneZip = browserEntries.some((b) => hasBundleFile(keys[b])) 87 | 88 | if (!hasAtLeastOneZip) { 89 | throw new Error("No artifact found for deployment") 90 | } 91 | 92 | if (keys.firefox && source) { 93 | keys.firefox.sourceZip = source 94 | } 95 | 96 | if (keys.edge && edgeNotes) { 97 | keys.edge.notes = edgeNotes 98 | } 99 | 100 | if (process.env.NODE_ENV === "test") { 101 | debug(JSON.stringify({ artifact, versionFile, verbose })) 102 | debug(browserEntries.join(",")) 103 | return 104 | } 105 | 106 | const deployPromises = browserEntries.map((browser) => { 107 | if (!hasBundleFile(keys[browser])) { 108 | return false 109 | } 110 | info(`${tag("🟡 QUEUE")} Prepare for ${browser} submission`) 111 | 112 | switch (browser) { 113 | case BrowserName.Chrome: 114 | return submitChrome(keys[browser]) 115 | case BrowserName.Firefox: 116 | return submitFirefox(keys[browser]) 117 | case BrowserName.Edge: 118 | return submitEdge(keys[browser]) 119 | case BrowserName.Itero: 120 | return submitItero(keys[browser]) 121 | } 122 | }) 123 | 124 | const results = await Promise.allSettled(deployPromises) 125 | 126 | results.forEach((result, index) => { 127 | if (result.status === "rejected") { 128 | setFailed(`${tag("🔴 ERROR")} ${result.reason}`) 129 | } else if (result.value) { 130 | info(`${tag("🟢 DONE")} ${browserEntries[index]} submission successful`) 131 | } 132 | }) 133 | } catch (error) { 134 | if (error instanceof Error) { 135 | setFailed(`${tag("🔴 ERROR")} ${error.message}`) 136 | } 137 | } 138 | } 139 | 140 | run() 141 | -------------------------------------------------------------------------------- /packages/bpp/tests/__snapshots__/main.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`get verbose flag successfully 1`] = ` 4 | "🟣 Plasmo Browser Platform Publish v2 5 | ::debug::{"artifact":"artifacts.zip","versionFile":"","verbose":true} 6 | ::debug::chrome 7 | " 8 | `; 9 | 10 | exports[`happy path 1`] = ` 11 | "🟣 Plasmo Browser Platform Publish v2 12 | ::debug::{"artifact":"artifacts.zip","versionFile":"","verbose":false} 13 | ::debug::chrome,firefox,edge 14 | " 15 | `; 16 | -------------------------------------------------------------------------------- /packages/bpp/tests/main.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@jest/globals" 2 | import { type ExecFileSyncOptions, execFileSync } from "child_process" 3 | import { readFile } from "fs/promises" 4 | import { resolve } from "path" 5 | import { cwd, env, execPath } from "process" 6 | 7 | const indexScript = resolve(cwd(), "dist", "index.js") 8 | 9 | test("happy path", async () => { 10 | const templateKeysPath = resolve(cwd(), "..", "..", "keys.template.json") 11 | 12 | const templateKeys = await readFile(templateKeysPath, "utf8") 13 | 14 | env["INPUT_KEYS"] = templateKeys 15 | env["INPUT_ARTIFACT"] = "artifacts.zip" 16 | 17 | const options: ExecFileSyncOptions = { 18 | env 19 | } 20 | 21 | const output = execFileSync(execPath, [indexScript], options).toString( 22 | "utf-8" 23 | ) 24 | expect(output).toMatchSnapshot() 25 | 26 | // try { 27 | // const output = execFileSync(execPath, [ip], options).toString("utf-8") 28 | // expect(output).toMatchSnapshot() 29 | // } catch (error: any) { 30 | // console.log(typeof error) 31 | 32 | // console.log(error["stdout"].toString("utf-8")) 33 | // } 34 | }) 35 | 36 | test("will fail if no zip found", async () => { 37 | env["INPUT_KEYS"] = `{"chrome": {}}` 38 | env["INPUT_ARTIFACT"] = "" 39 | 40 | const options: ExecFileSyncOptions = { 41 | env 42 | } 43 | 44 | const tExec = () => execFileSync(execPath, [indexScript], options) 45 | 46 | expect(tExec).toThrowError() 47 | }) 48 | 49 | test("get verbose flag successfully", async () => { 50 | env["INPUT_KEYS"] = `{"chrome": {}}` 51 | env["INPUT_ZIP"] = "artifacts.zip" 52 | env["INPUT_VERBOSE"] = "true" 53 | 54 | const options: ExecFileSyncOptions = { 55 | env 56 | } 57 | 58 | const output = execFileSync(execPath, [indexScript], options).toString( 59 | "utf-8" 60 | ) 61 | expect(output).toMatchSnapshot() 62 | }) 63 | -------------------------------------------------------------------------------- /packages/bpp/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@plasmo/config/ts/cli.json", 3 | "compilerOptions": { 4 | "strict": true /* Enable all strict type-checking options. */, 5 | "noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */ 6 | }, 7 | "include": ["src/**/*", "tests/**/*.test.ts", "./jest.config.mjs"], 8 | "exclude": ["node_modules"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/esbuild-plugins/inline-fflate.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { readFile } from "fs/promises" 3 | import { resolve } from "path" 4 | 5 | /** 6 | * @return {import("esbuild").Plugin} 7 | */ 8 | export default function inlinePlugin({ 9 | filter = /^inline:/, 10 | 11 | namespace = "_" + Math.random().toString(36).substring(2, 9) 12 | }) { 13 | return { 14 | name: "esbuild-inline-fflate", 15 | setup(build) { 16 | build.onResolve({ filter }, (args) => { 17 | const realPath = args.path.replace(filter, "") 18 | return { 19 | path: resolve(args.resolveDir, realPath), 20 | namespace 21 | } 22 | }) 23 | 24 | build.onLoad({ filter: /.*/, namespace }, async (args) => { 25 | let contents = await readFile(args.path, "utf8") 26 | 27 | // Use fflate to base64 the whole director to be inlined 28 | 29 | // Then in the client code, we inflate it and write it down to the file system 30 | 31 | return { 32 | contents, 33 | loader: "text" 34 | } 35 | }) 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/esbuild-plugins/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@plasmo/esbuild-plugins", 3 | "version": "0.0.3", 4 | "private": true, 5 | "type": "module", 6 | "files": [ 7 | "inline-fflate.mjs" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "packages/*" 3 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turborepo.org/schema.json", 3 | "tasks": { 4 | "package": { 5 | "dependsOn": ["^build"], 6 | "outputs": ["dist/**"] 7 | }, 8 | "build": { 9 | "dependsOn": ["^build"], 10 | "outputs": ["dist/**"] 11 | }, 12 | "dev": { 13 | "dependsOn": ["^build"], 14 | "cache": false 15 | } 16 | } 17 | } 18 | --------------------------------------------------------------------------------