From 2f5568fb3790c2d2712f2d0a87efd806d78d78f3 Mon Sep 17 00:00:00 2001 From: wt Date: Fri, 9 Oct 2026 16:34:05 +0700 Subject: [PATCH] ci: build and publish extension packages from GitHub Actions - build Firefox XPI and Chrome ZIP on every push - upload packages as downloadable workflow artifacts - sign and submit Firefox releases to AMO - upload and publish Chrome releases to Chrome Web Store - attach signed packages to GitHub release cards - document required publishing secrets and release workflow --- .github/workflows/release.yml | 196 ++++++++++++++++++++++++++++++++++ 1 file changed, 196 insertions(+) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..06549fc --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,196 @@ +name: Build and publish extension + +on: + push: + branches: + - "**" + release: + types: [published] + workflow_dispatch: + inputs: + version: + description: "Version to validate (for example 1.0.1)" + 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+){1,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: | + mkdir -p dist/firefox-source + rsync -a \ + --exclude '.git/' \ + --exclude '.github/' \ + --exclude 'web-ext-artifacts/' \ + --exclude 'manifest.chrome.json' \ + --exclude 'background-chrome.js' \ + --exclude 'background-service-worker.js' \ + --exclude 'browser-api.js' \ + --exclude 'scripts/' \ + ./ dist/firefox-source/ + + - 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: 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 + run: | + npm install --global web-ext + mkdir -p dist/firefox-source + rsync -a \ + --exclude '.git/' \ + --exclude '.github/' \ + --exclude 'web-ext-artifacts/' \ + --exclude 'manifest.chrome.json' \ + --exclude 'background-chrome.js' \ + --exclude 'background-service-worker.js' \ + --exclude 'browser-api.js' \ + --exclude 'scripts/' \ + ./ dist/firefox-source/ + + - name: Sign and submit Firefox package to AMO + 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" \ + --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 + 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: Attach signed Firefox and Chrome packages to release + env: + GH_TOKEN: ${{ github.token }} + run: | + gh release upload "${{ github.event.release.tag_name }}" \ + dist/packages/proxyswitcherforkbywt-firefox-signed.xpi \ + "$chrome_zip" \ + --clobber + + - name: Upload signed packages as workflow artifacts + uses: actions/upload-artifact@v4 + with: + name: signed-extension-packages-${{ github.event.release.tag_name }} + if-no-files-found: error + path: | + dist/packages/proxyswitcherforkbywt-firefox-signed.xpi + dist/packages/**/proxyswitcherforkbywt-chrome.zip