chore(release): v0.8.37 #54
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Extension Release Pipeline | |
| on: | |
| push: | |
| tags: | |
| - "v*.*.*" | |
| workflow_dispatch: | |
| inputs: | |
| publish: | |
| description: "Publish to stores (false = build only)" | |
| type: boolean | |
| default: true | |
| permissions: | |
| contents: read | |
| jobs: | |
| build: | |
| name: Build Extension | |
| runs-on: ubuntu-latest | |
| outputs: | |
| version: ${{ steps.version.outputs.version }} | |
| steps: | |
| - name: Load environment variables | |
| env: | |
| ENV_CONTENT: ${{ secrets.ENV_FILE }} | |
| run: | | |
| while IFS='=' read -r key value; do | |
| [[ -z "$key" || "$key" =~ ^# ]] && continue | |
| [[ -n "$value" ]] && echo "::add-mask::$value" | |
| echo "${key}=${value}" >> "$GITHUB_ENV" | |
| done <<< "$ENV_CONTENT" | |
| - uses: actions/checkout@v7 | |
| - uses: pnpm/action-setup@v6 | |
| - uses: actions/setup-node@v7 | |
| with: | |
| node-version: "22" | |
| cache: "pnpm" | |
| - name: Install dependencies | |
| run: pnpm install --frozen-lockfile | |
| - name: Extract and validate version | |
| id: version | |
| run: | | |
| MANIFEST_VERSION=$(node -p "JSON.parse(require('fs').readFileSync('apps/extension/src/manifest.chrome.json', 'utf-8')).version") | |
| if [[ "$GITHUB_REF_TYPE" == "tag" ]]; then | |
| TAG_VERSION="${GITHUB_REF_NAME#v}" | |
| if [[ "$TAG_VERSION" != "$MANIFEST_VERSION" ]]; then | |
| echo "::error::Tag version ($TAG_VERSION) does not match manifest version ($MANIFEST_VERSION)" | |
| exit 1 | |
| fi | |
| VERSION="$TAG_VERSION" | |
| else | |
| VERSION="$MANIFEST_VERSION" | |
| fi | |
| echo "version=$VERSION" >> "$GITHUB_OUTPUT" | |
| echo "Building version: $VERSION" | |
| - name: Build extension | |
| run: pnpm turbo build --filter=@marksyncr/extension... | |
| env: | |
| NODE_ENV: production | |
| NEXT_PUBLIC_SUPABASE_URL: ${{ env.NEXT_PUBLIC_SUPABASE_URL }} | |
| NEXT_PUBLIC_SUPABASE_ANON_KEY: ${{ env.NEXT_PUBLIC_SUPABASE_ANON_KEY }} | |
| NEXT_PUBLIC_APP_URL: ${{ env.NEXT_PUBLIC_APP_URL }} | |
| - name: Verify build output | |
| run: | | |
| echo "Build artifacts:" | |
| ls -lh apps/extension/dist/*.zip | |
| test -f apps/extension/dist/marksyncr-chrome.zip || { echo "::error::Chrome ZIP not found"; exit 1; } | |
| test -f apps/extension/dist/marksyncr-firefox.zip || { echo "::error::Firefox ZIP not found"; exit 1; } | |
| cp apps/extension/dist/marksyncr-chrome.zip apps/extension/dist/marksyncr-edge.zip | |
| - name: Upload Chrome ZIP | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: marksyncr-chrome | |
| path: apps/extension/dist/marksyncr-chrome.zip | |
| retention-days: 30 | |
| - name: Upload Firefox ZIP | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: marksyncr-firefox | |
| path: apps/extension/dist/marksyncr-firefox.zip | |
| retention-days: 30 | |
| - name: Upload Edge ZIP | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: marksyncr-edge | |
| path: apps/extension/dist/marksyncr-edge.zip | |
| retention-days: 30 | |
| chrome-release: | |
| name: Publish to Chrome Web Store | |
| needs: build | |
| if: | | |
| github.ref_type == 'tag' || | |
| (github.event_name == 'workflow_dispatch' && inputs.publish) | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Load environment variables | |
| env: | |
| ENV_CONTENT: ${{ secrets.ENV_FILE }} | |
| run: | | |
| while IFS='=' read -r key value; do | |
| [[ -z "$key" || "$key" =~ ^# ]] && continue | |
| [[ -n "$value" ]] && echo "::add-mask::$value" | |
| echo "${key}=${value}" >> "$GITHUB_ENV" | |
| done <<< "$ENV_CONTENT" | |
| - name: Download artifact | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: marksyncr-chrome | |
| - name: Authenticate with Chrome Web Store | |
| id: auth | |
| run: | | |
| RESPONSE=$(curl -sS -X POST "https://oauth2.googleapis.com/token" \ | |
| -d "client_id=$CHROME_CLIENT_ID" \ | |
| -d "client_secret=$CHROME_CLIENT_SECRET" \ | |
| -d "refresh_token=$CHROME_REFRESH_TOKEN" \ | |
| -d "grant_type=refresh_token") | |
| ACCESS_TOKEN=$(echo "$RESPONSE" | jq -r '.access_token // empty') | |
| if [[ -z "$ACCESS_TOKEN" ]]; then | |
| echo "::error::Failed to obtain Chrome Web Store access token" | |
| echo "$RESPONSE" | jq '{error, error_description}' 2>/dev/null || echo "$RESPONSE" | |
| exit 1 | |
| fi | |
| echo "::add-mask::$ACCESS_TOKEN" | |
| echo "token=$ACCESS_TOKEN" >> "$GITHUB_OUTPUT" | |
| echo "Chrome Web Store authentication successful" | |
| - name: Upload to Chrome Web Store | |
| run: | | |
| RESPONSE=$(curl -sS \ | |
| -X PUT \ | |
| -H "Authorization: Bearer ${{ steps.auth.outputs.token }}" \ | |
| -H "x-goog-api-version: 2" \ | |
| -T marksyncr-chrome.zip \ | |
| "https://www.googleapis.com/upload/chromewebstore/v1.1/items/$CHROME_EXTENSION_ID") | |
| echo "Upload response:" | |
| echo "$RESPONSE" | jq . | |
| UPLOAD_STATE=$(echo "$RESPONSE" | jq -r '.uploadState') | |
| if [[ "$UPLOAD_STATE" != "SUCCESS" ]]; then | |
| echo "::error::Chrome upload failed with state: $UPLOAD_STATE" | |
| echo "$RESPONSE" | jq '.itemError // .' | |
| exit 1 | |
| fi | |
| echo "Chrome extension uploaded successfully" | |
| - name: Submit to Chrome Web Store for review | |
| run: | | |
| RESPONSE=$(curl -sS \ | |
| -X POST \ | |
| -H "Authorization: Bearer ${{ steps.auth.outputs.token }}" \ | |
| -H "x-goog-api-version: 2" \ | |
| -H "Content-Length: 0" \ | |
| "https://www.googleapis.com/chromewebstore/v1.1/items/$CHROME_EXTENSION_ID/publish") | |
| echo "Publish response:" | |
| echo "$RESPONSE" | jq . | |
| STATUS=$(echo "$RESPONSE" | jq -r '.status[0] // empty') | |
| ERROR_MSG=$(echo "$RESPONSE" | jq -r '.error.message // empty') | |
| if [[ "$STATUS" == "OK" || "$STATUS" == "PUBLISHED_WITH_FRICTION_WARNING" || "$STATUS" == "PENDING_REVIEW" ]]; then | |
| echo "Chrome extension v${{ needs.build.outputs.version }} submitted (status: $STATUS)" | |
| exit 0 | |
| fi | |
| # The new package is uploaded as a draft regardless of this step. Some | |
| # publish failures are one-time account-level gates the owner must clear | |
| # in the Chrome dashboard (e.g. the mandatory "Privacy practices" form). | |
| # Those shouldn't fail the release — the draft is ready to submit — so | |
| # warn loudly and pass. Genuinely unexpected errors still fail. | |
| if echo "$ERROR_MSG" | grep -qiE "publish condition not met|privacy"; then | |
| echo "::warning title=Chrome publish gated::Draft v${{ needs.build.outputs.version }} uploaded but not submitted — clear the gate in the Chrome dashboard: $ERROR_MSG" | |
| exit 0 | |
| fi | |
| echo "::error::Chrome publish failed with status: ${STATUS:-none}" | |
| echo "$RESPONSE" | jq '.statusDetail // .error // .' | |
| exit 1 | |
| firefox-release: | |
| name: Publish to Firefox Add-ons | |
| needs: build | |
| if: | | |
| github.ref_type == 'tag' || | |
| (github.event_name == 'workflow_dispatch' && inputs.publish) | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Load environment variables | |
| env: | |
| ENV_CONTENT: ${{ secrets.ENV_FILE }} | |
| run: | | |
| while IFS='=' read -r key value; do | |
| [[ -z "$key" || "$key" =~ ^# ]] && continue | |
| [[ -n "$value" ]] && echo "::add-mask::$value" | |
| echo "${key}=${value}" >> "$GITHUB_ENV" | |
| done <<< "$ENV_CONTENT" | |
| - name: Download artifact | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: marksyncr-firefox | |
| - name: Unpack extension | |
| run: | | |
| mkdir -p extension | |
| unzip marksyncr-firefox.zip -d extension | |
| echo "Extension contents:" | |
| ls -la extension/ | |
| - uses: actions/setup-node@v7 | |
| with: | |
| node-version: "22" | |
| - name: Install web-ext | |
| run: npm install -g web-ext | |
| - name: Submit to AMO | |
| run: | | |
| web-ext sign \ | |
| --source-dir ./extension \ | |
| --artifacts-dir ./artifacts \ | |
| --api-key "$FIREFOX_JWT_ISSUER" \ | |
| --api-secret "$FIREFOX_JWT_SECRET" \ | |
| --channel listed \ | |
| --approval-timeout 0 | |
| echo "Firefox extension v${{ needs.build.outputs.version }} submitted to AMO" | |
| edge-release: | |
| name: Publish to Edge Add-ons | |
| needs: build | |
| if: | | |
| github.ref_type == 'tag' || | |
| (github.event_name == 'workflow_dispatch' && inputs.publish) | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Load environment variables | |
| env: | |
| ENV_CONTENT: ${{ secrets.ENV_FILE }} | |
| run: | | |
| while IFS='=' read -r key value; do | |
| [[ -z "$key" || "$key" =~ ^# ]] && continue | |
| [[ -n "$value" ]] && echo "::add-mask::$value" | |
| echo "${key}=${value}" >> "$GITHUB_ENV" | |
| done <<< "$ENV_CONTENT" | |
| - name: Download artifact | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: marksyncr-edge | |
| - name: Upload to Edge Add-ons | |
| id: upload | |
| run: | | |
| RESPONSE=$(curl -sS -w "\n%{http_code}" \ | |
| -X POST \ | |
| -H "Authorization: ApiKey $EDGE_API_KEY" \ | |
| -H "X-ClientID: $EDGE_CLIENT_ID" \ | |
| -H "Content-Type: application/zip" \ | |
| -T marksyncr-edge.zip \ | |
| "https://api.addons.microsoftedge.microsoft.com/v1/products/$EDGE_PRODUCT_ID/submissions/draft/package") | |
| HTTP_CODE=$(echo "$RESPONSE" | tail -1) | |
| BODY=$(echo "$RESPONSE" | head -n -1) | |
| echo "Upload response (HTTP $HTTP_CODE):" | |
| echo "$BODY" | jq . 2>/dev/null || echo "$BODY" | |
| # Expired/invalid Edge publisher credentials are an ops issue, not a | |
| # release blocker — warn and skip the rest of the Edge job rather than | |
| # failing the whole release. Refresh the EDGE_* secrets to re-enable. | |
| if [[ "$HTTP_CODE" == "401" || "$HTTP_CODE" == "403" ]]; then | |
| echo "::warning title=Edge publish skipped::Edge auth failed (HTTP $HTTP_CODE) — refresh EDGE_API_KEY/EDGE_CLIENT_ID secrets to re-enable Edge publishing." | |
| echo "skipped=true" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| if [[ "$HTTP_CODE" != "202" && "$HTTP_CODE" != "200" ]]; then | |
| echo "::error::Edge upload failed with HTTP $HTTP_CODE" | |
| exit 1 | |
| fi | |
| OPERATION_ID=$(echo "$BODY" | jq -r '.operationID // empty') | |
| if [[ -z "$OPERATION_ID" ]]; then | |
| OPERATION_ID=$(echo "$RESPONSE" | grep -oP '"operationID"\s*:\s*"\K[^"]+' || true) | |
| fi | |
| echo "operation_id=$OPERATION_ID" >> "$GITHUB_OUTPUT" | |
| echo "Edge extension uploaded, operation: $OPERATION_ID" | |
| - name: Wait for upload processing | |
| if: steps.upload.outputs.skipped != 'true' | |
| run: | | |
| OPERATION_ID="${{ steps.upload.outputs.operation_id }}" | |
| if [[ -z "$OPERATION_ID" ]]; then | |
| echo "No operation ID, skipping status check" | |
| exit 0 | |
| fi | |
| for i in $(seq 1 30); do | |
| sleep 10 | |
| RESPONSE=$(curl -sS \ | |
| -H "Authorization: ApiKey $EDGE_API_KEY" \ | |
| -H "X-ClientID: $EDGE_CLIENT_ID" \ | |
| "https://api.addons.microsoftedge.microsoft.com/v1/products/$EDGE_PRODUCT_ID/submissions/draft/package/operations/$OPERATION_ID") | |
| STATUS=$(echo "$RESPONSE" | jq -r '.status // empty') | |
| echo "Attempt $i/30: status=$STATUS" | |
| if [[ "$STATUS" == "Succeeded" ]]; then | |
| echo "Upload processing complete" | |
| break | |
| elif [[ "$STATUS" == "Failed" ]]; then | |
| echo "::error::Edge upload processing failed" | |
| echo "$RESPONSE" | jq . | |
| exit 1 | |
| fi | |
| done | |
| - name: Publish submission | |
| if: steps.upload.outputs.skipped != 'true' | |
| run: | | |
| RESPONSE=$(curl -sS -w "\n%{http_code}" \ | |
| -X POST \ | |
| -H "Authorization: ApiKey $EDGE_API_KEY" \ | |
| -H "X-ClientID: $EDGE_CLIENT_ID" \ | |
| -H "Content-Type: application/json" \ | |
| -d '{}' \ | |
| "https://api.addons.microsoftedge.microsoft.com/v1/products/$EDGE_PRODUCT_ID/submissions") | |
| HTTP_CODE=$(echo "$RESPONSE" | tail -1) | |
| BODY=$(echo "$RESPONSE" | head -n -1) | |
| echo "Publish response (HTTP $HTTP_CODE):" | |
| echo "$BODY" | jq . 2>/dev/null || echo "$BODY" | |
| if [[ "$HTTP_CODE" != "202" && "$HTTP_CODE" != "200" ]]; then | |
| echo "::error::Edge publish failed with HTTP $HTTP_CODE" | |
| exit 1 | |
| fi | |
| echo "Edge extension v${{ needs.build.outputs.version }} submitted for review" |