name: Build and publish extension on: push: branches: - "**" release: types: [published] workflow_dispatch: inputs: version: description: "Version to validate (for example 20261009)" required: true type: string permissions: contents: read jobs: build: name: Build extension packages runs-on: ubuntu-latest steps: - name: Check out source uses: actions/checkout@v4 with: ref: ${{ github.event.release.tag_name || github.ref }} - name: Set up Node.js uses: actions/setup-node@v4 with: node-version: 22 - name: Set up Python uses: actions/setup-python@v5 with: python-version: "3.x" - name: Validate release version if: github.event_name == 'release' || github.event_name == 'workflow_dispatch' env: RELEASE_VERSION: ${{ github.event.release.tag_name || inputs.version }} run: | python - <<'PY' import json, os, re expected = os.environ["RELEASE_VERSION"].removeprefix("v") for path in ("manifest.json", "manifest.chrome.json"): actual = json.load(open(path, encoding="utf-8"))["version"] if actual != expected: raise SystemExit(f"{path}: version {actual!r} does not match release {expected!r}") if not re.fullmatch(r"\d+(\.\d+){0,3}", expected): raise SystemExit(f"Release version must be numeric, got {expected!r}") PY - name: Install web-ext run: npm install --global web-ext - name: Prepare Firefox source run: | python - <<'PY' from pathlib import Path import shutil source = Path('.') target = Path('dist/firefox-source') excluded = {'.git', '.github', 'dist', 'web-ext-artifacts', 'scripts', 'manifest.chrome.json', 'background-chrome.js', 'background-service-worker.js', 'browser-api.js'} target.mkdir(parents=True, exist_ok=True) for item in source.iterdir(): if item.name in excluded: continue destination = target / item.name if item.is_dir(): shutil.copytree(item, destination, dirs_exist_ok=True) else: shutil.copy2(item, destination) PY - name: Build Firefox package run: | web-ext build \ --source-dir dist/firefox-source \ --artifacts-dir dist/firefox \ --filename proxyswitcherforkbywt-firefox.xpi \ --overwrite-dest - name: Build Chrome package run: python scripts/build-chrome.py - name: Upload packages for this push uses: actions/upload-artifact@v4 with: name: extension-packages-${{ github.sha }} if-no-files-found: error path: | dist/firefox/proxyswitcherforkbywt-firefox.xpi web-ext-artifacts/proxyswitcherforkbywt-chrome.zip publish: name: Publish stores and attach release assets if: github.event_name == 'release' && github.event.action == 'published' needs: build runs-on: ubuntu-latest permissions: contents: write env: AMO_JWT_ISSUER: ${{ secrets.AMO_JWT_ISSUER }} AMO_JWT_SECRET: ${{ secrets.AMO_JWT_SECRET }} CWS_PUBLISHER_ID: ${{ secrets.CWS_PUBLISHER_ID }} CWS_EXTENSION_ID: ${{ secrets.CWS_EXTENSION_ID }} CWS_CLIENT_ID: ${{ secrets.CWS_CLIENT_ID }} CWS_CLIENT_SECRET: ${{ secrets.CWS_CLIENT_SECRET }} CWS_REFRESH_TOKEN: ${{ secrets.CWS_REFRESH_TOKEN }} steps: - name: Check out release source uses: actions/checkout@v4 with: ref: ${{ github.event.release.tag_name }} - name: Check store credentials id: credentials run: | python - <<'PY' import os stores = { 'firefox': ('AMO_JWT_ISSUER', 'AMO_JWT_SECRET'), 'chrome': ('CWS_PUBLISHER_ID', 'CWS_EXTENSION_ID', 'CWS_CLIENT_ID', 'CWS_CLIENT_SECRET', 'CWS_REFRESH_TOKEN'), } with open(os.environ['GITHUB_OUTPUT'], 'a') as output: for store, names in stores.items(): missing = [name for name in names if not os.environ.get(name, '').strip()] output.write(f"{store}={'false' if missing else 'true'}\n") if missing: print(f"Skipping {store} deployment: missing credentials: {', '.join(missing)}") else: print(f"{store} deployment enabled") PY - name: Set up Node.js uses: actions/setup-node@v4 with: node-version: 22 - name: Download build packages uses: actions/download-artifact@v4 with: name: extension-packages-${{ github.sha }} path: dist/packages - name: Set up Firefox signing tools if: steps.credentials.outputs.firefox == 'true' run: | npm install --global web-ext python - <<'PY' from pathlib import Path import shutil source = Path('.') target = Path('dist/firefox-source') excluded = {'.git', '.github', 'dist', 'web-ext-artifacts', 'scripts', 'manifest.chrome.json', 'background-chrome.js', 'background-service-worker.js', 'browser-api.js'} target.mkdir(parents=True, exist_ok=True) for item in source.iterdir(): if item.name in excluded: continue destination = target / item.name if item.is_dir(): shutil.copytree(item, destination, dirs_exist_ok=True) else: shutil.copy2(item, destination) PY - name: Sign and submit Firefox package to AMO if: steps.credentials.outputs.firefox == 'true' run: | test -n "$AMO_JWT_ISSUER" || { echo 'Missing AMO_JWT_ISSUER secret'; exit 1; } test -n "$AMO_JWT_SECRET" || { echo 'Missing AMO_JWT_SECRET secret'; exit 1; } web-ext sign \ --source-dir dist/firefox-source \ --artifacts-dir dist/firefox-signed \ --api-key "$AMO_JWT_ISSUER" \ --api-secret "$AMO_JWT_SECRET" \ --amo-metadata amo-metadata.json \ --channel listed \ --approval-timeout 900000 \ --no-input firefox_xpi=$(find dist/firefox-signed -maxdepth 1 -name '*.xpi' -print -quit) test -n "$firefox_xpi" || { echo 'AMO did not return a signed XPI'; exit 1; } cp "$firefox_xpi" dist/packages/proxyswitcherforkbywt-firefox-signed.xpi - name: Upload Chrome package to Chrome Web Store if: steps.credentials.outputs.chrome == 'true' run: | set -euo pipefail for name in CWS_PUBLISHER_ID CWS_EXTENSION_ID CWS_CLIENT_ID CWS_CLIENT_SECRET CWS_REFRESH_TOKEN; do test -n "${!name}" || { echo "Missing $name secret"; exit 1; } done token=$(curl --fail --silent --show-error \ --data-urlencode "client_id=$CWS_CLIENT_ID" \ --data-urlencode "client_secret=$CWS_CLIENT_SECRET" \ --data-urlencode "refresh_token=$CWS_REFRESH_TOKEN" \ --data-urlencode 'grant_type=refresh_token' \ https://oauth2.googleapis.com/token | python -c 'import json,sys; print(json.load(sys.stdin)["access_token"])') chrome_zip=$(find dist/packages -name 'proxyswitcherforkbywt-chrome.zip' -print -quit) test -f "$chrome_zip" || { echo "Missing Chrome package: $chrome_zip"; exit 1; } curl --fail --silent --show-error \ -X POST \ -H "Authorization: Bearer $token" \ -H 'Content-Type: application/zip' \ --data-binary "@$chrome_zip" \ "https://chromewebstore.googleapis.com/upload/v2/publishers/$CWS_PUBLISHER_ID/items/$CWS_EXTENSION_ID:upload" curl --fail --silent --show-error \ -X POST \ -H "Authorization: Bearer $token" \ "https://chromewebstore.googleapis.com/v2/publishers/$CWS_PUBLISHER_ID/items/$CWS_EXTENSION_ID:publish" - name: Prepare release assets run: | mkdir -p dist/release-assets chrome_zip=$(find dist/packages -name 'proxyswitcherforkbywt-chrome.zip' -print -quit) test -f "$chrome_zip" cp "$chrome_zip" dist/release-assets/ firefox_xpi=dist/packages/proxyswitcherforkbywt-firefox-signed.xpi if [ ! -f "$firefox_xpi" ]; then firefox_xpi=$(find dist/packages -name 'proxyswitcherforkbywt-firefox.xpi' -print -quit) fi test -f "$firefox_xpi" cp "$firefox_xpi" dist/release-assets/ - name: Attach Firefox and Chrome packages to release env: GH_TOKEN: ${{ github.token }} RELEASE_TAG: ${{ github.event.release.tag_name }} run: | gh release upload "$RELEASE_TAG" \ dist/release-assets/* \ --clobber - name: Upload release packages as workflow artifacts uses: actions/upload-artifact@v4 with: name: release-extension-packages-${{ github.event.release.tag_name }} if-no-files-found: error path: dist/release-assets/*