diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0558903..01fe499 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -12,10 +12,10 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v7 with: node-version: '24' cache: 'npm' @@ -36,7 +36,7 @@ jobs: run: npm run build - name: Upload extension artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: extension-build path: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a40d626..10098da 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,10 +14,10 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v7 with: node-version: '24' cache: 'npm' @@ -119,7 +119,7 @@ jobs: # GitHub Release # ========================================================================= - name: Create GitHub Release - uses: softprops/action-gh-release@v2 + uses: softprops/action-gh-release@v3 with: generate_release_notes: true files: | @@ -155,13 +155,11 @@ jobs: # Firefox Add-ons Publishing # ========================================================================= - name: Publish to Firefox Add-ons - uses: browser-actions/release-firefox-addon@v0.2.1 - with: - addon-id: ${{ secrets.FIREFOX_ADDON_ID }} - addon-path: light-session-${{ steps.version.outputs.VERSION }}-firefox.zip - source-path: light-session-${{ steps.version.outputs.VERSION }}-source.zip - license: MIT - approval-note: | + env: + VERSION: ${{ steps.version.outputs.VERSION }} + WEB_EXT_API_KEY: ${{ secrets.FIREFOX_API_ISSUER }} + WEB_EXT_API_SECRET: ${{ secrets.FIREFOX_API_SECRET }} + APPROVAL_NOTES: | Source archive for this exact version is attached. Build environment: @@ -176,18 +174,94 @@ jobs: cd extension && zip -r ../light-session-VERSION-firefox.zip manifest.json dist/ popup/ icons/ -x "*.map" See AMO-SOURCE-README.md in the source archive for details. - auth-api-issuer: ${{ secrets.FIREFOX_API_ISSUER }} - auth-api-secret: ${{ secrets.FIREFOX_API_SECRET }} - release-note: "See release notes at https://github.com/11me/light-session/releases/tag/v${{ steps.version.outputs.VERSION }}" + run: | + set -euo pipefail + + # Skip if this version was already submitted (e.g. re-running a failed job) + JWT=$(node -e ' + const c = require("crypto"); + const b64 = (o) => Buffer.from(JSON.stringify(o)).toString("base64url"); + const now = Math.floor(Date.now() / 1000); + const data = b64({ alg: "HS256", typ: "JWT" }) + "." + + b64({ iss: process.env.WEB_EXT_API_KEY, jti: c.randomUUID(), iat: now, exp: now + 60 }); + const sig = c.createHmac("sha256", process.env.WEB_EXT_API_SECRET).update(data).digest("base64url"); + console.log(data + "." + sig); + ') + echo "::add-mask::$JWT" + ADDON_ID=$(jq -er .browser_specific_settings.gecko.id extension/manifest.firefox.json) + STATUS=$(curl -sS -o /dev/null -w '%{http_code}' -H "Authorization: JWT $JWT" \ + "https://addons.mozilla.org/api/v5/addons/addon/${ADDON_ID}/versions/v${VERSION}/") + case "$STATUS" in + 200) echo "Version ${VERSION} already exists on AMO, skipping"; exit 0 ;; + 404) ;; + *) echo "::error::AMO version lookup failed: HTTP $STATUS"; exit 1 ;; + esac + + # Sign from the exact files in the Firefox zip (extension/ now holds the Chrome build) + mkdir -p amo-upload + unzip -q "light-session-${VERSION}-firefox.zip" -d amo-upload + + jq -n \ + --arg notes "$APPROVAL_NOTES" \ + --arg release "See release notes at https://github.com/11me/light-session/releases/tag/v${VERSION}" \ + '{version: {license: "MIT", approval_notes: $notes, release_notes: {"en-US": $release}}}' \ + > amo-metadata.json + + # Add-on ID comes from browser_specific_settings.gecko.id in the manifest + npx web-ext sign \ + --source-dir amo-upload \ + --channel listed \ + --amo-metadata amo-metadata.json \ + --upload-source-code "light-session-${VERSION}-source.zip" \ + --approval-timeout 0 # ========================================================================= - # Chrome Web Store Publishing + # Chrome Web Store Publishing (API v2) # ========================================================================= - name: Publish to Chrome Web Store - uses: browser-actions/release-chrome-extension@v0.2.1 - with: - extension-id: ${{ secrets.CHROME_EXTENSION_ID }} - extension-path: light-session-${{ steps.version.outputs.VERSION }}-chrome.zip - oauth-client-id: ${{ secrets.CHROME_CLIENT_ID }} - oauth-client-secret: ${{ secrets.CHROME_CLIENT_SECRET }} - oauth-refresh-token: ${{ secrets.CHROME_REFRESH_TOKEN }} + env: + VERSION: ${{ steps.version.outputs.VERSION }} + PUBLISHER_ID: ${{ secrets.CHROME_PUBLISHER_ID }} + EXTENSION_ID: ${{ secrets.CHROME_EXTENSION_ID }} + CLIENT_ID: ${{ secrets.CHROME_CLIENT_ID }} + CLIENT_SECRET: ${{ secrets.CHROME_CLIENT_SECRET }} + REFRESH_TOKEN: ${{ secrets.CHROME_REFRESH_TOKEN }} + run: | + set -euo pipefail + ITEM="publishers/${PUBLISHER_ID}/items/${EXTENSION_ID}" + API=https://chromewebstore.googleapis.com + + ACCESS_TOKEN=$(curl -sS --fail-with-body https://oauth2.googleapis.com/token \ + -d client_id="$CLIENT_ID" \ + -d client_secret="$CLIENT_SECRET" \ + -d refresh_token="$REFRESH_TOKEN" \ + -d grant_type=refresh_token | jq -er .access_token) + echo "::add-mask::$ACCESS_TOKEN" + AUTH=(-H "Authorization: Bearer $ACCESS_TOKEN") + + RESPONSE=$(curl -sS --fail-with-body "${AUTH[@]}" -X POST \ + -T "light-session-${VERSION}-chrome.zip" \ + "$API/upload/v2/${ITEM}:upload") + echo "$RESPONSE" + STATE=$(jq -r .uploadState <<< "$RESPONSE") + + for _ in $(seq 1 30); do + [ "$STATE" != "IN_PROGRESS" ] && break + sleep 10 + RESPONSE=$(curl -sS --fail-with-body "${AUTH[@]}" "$API/v2/${ITEM}:fetchStatus") + echo "$RESPONSE" + STATE=$(jq -r .lastAsyncUploadState <<< "$RESPONSE") + done + if [ "$STATE" != "SUCCEEDED" ]; then + echo "::error::Chrome Web Store upload failed: $STATE" + exit 1 + fi + + RESPONSE=$(curl -sS --fail-with-body "${AUTH[@]}" -X POST \ + -H "Content-Type: application/json" -d '{}' \ + "$API/v2/${ITEM}:publish") + echo "$RESPONSE" + case "$(jq -r .state <<< "$RESPONSE")" in + PENDING_REVIEW|STAGED|PUBLISHED|PUBLISHED_TO_TESTERS) ;; + *) echo "::error::Chrome Web Store publish failed"; exit 1 ;; + esac