Merge pull request #142 from Tencent/chore/release-ext-0.1.7 #6
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: Release Extension | |
| on: | |
| push: | |
| tags: | |
| - ext-v* | |
| workflow_dispatch: | |
| permissions: | |
| contents: write | |
| concurrency: | |
| group: release-extension-${{ github.ref }} | |
| cancel-in-progress: false | |
| jobs: | |
| resolve: | |
| runs-on: ubuntu-latest | |
| outputs: | |
| version: ${{ steps.version.outputs.version }} | |
| tag: ${{ steps.version.outputs.tag }} | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Resolve extension version | |
| id: version | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then | |
| VERSION="$(node -p "require('./apps/extension/package.json').version")" | |
| else | |
| VERSION="${GITHUB_REF_NAME#ext-v}" | |
| fi | |
| VERSION="${VERSION#v}" | |
| echo "version=${VERSION}" >> "$GITHUB_OUTPUT" | |
| echo "tag=ext-v${VERSION}" >> "$GITHUB_OUTPUT" | |
| guard: | |
| needs: resolve | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Verify tag matches extension package.json version | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| PKG_VERSION="$(node -p "require('./apps/extension/package.json').version")" | |
| RELEASE_VERSION="${{ needs.resolve.outputs.version }}" | |
| if [ "$PKG_VERSION" != "$RELEASE_VERSION" ]; then | |
| echo "apps/extension/package.json version (${PKG_VERSION}) does not match release version (${RELEASE_VERSION})" | |
| exit 1 | |
| fi | |
| echo "Version guard passed: ${RELEASE_VERSION}" | |
| build-extension: | |
| needs: [resolve, guard] | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - uses: pnpm/action-setup@v6 | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version: 22 | |
| cache: pnpm | |
| - name: Install dependencies | |
| run: pnpm install --frozen-lockfile | |
| - name: Build extension zip | |
| run: pnpm ext:build:zip | |
| - name: Locate and rename extension zip | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| VERSION="${{ needs.resolve.outputs.version }}" | |
| ZIP="" | |
| for dir in apps/extension/dist apps/extension/.output; do | |
| if [ ! -d "$dir" ]; then | |
| continue | |
| fi | |
| candidate="$(find "$dir" -maxdepth 1 -name '*chrome*.zip' -type f 2>/dev/null | head -n 1 || true)" | |
| if [ -n "$candidate" ]; then | |
| ZIP="$candidate" | |
| break | |
| fi | |
| done | |
| if [ -z "$ZIP" ]; then | |
| echo "Could not find extension zip under apps/extension/dist or apps/extension/.output" | |
| find apps/extension -name '*.zip' -type f || true | |
| exit 1 | |
| fi | |
| OUT="browser-skill-extension-v${VERSION}-chrome.zip" | |
| cp "$ZIP" "$OUT" | |
| echo "ARCHIVE=$OUT" >> "$GITHUB_ENV" | |
| echo "Packaged ${ZIP} -> ${OUT}" | |
| - name: Upload artifact | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: extension-zip | |
| path: ${{ env.ARCHIVE }} | |
| if-no-files-found: error | |
| release: | |
| needs: [resolve, build-extension] | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Download build artifact | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: extension-zip | |
| path: dist | |
| - name: Publish GitHub Release | |
| uses: softprops/action-gh-release@v3 | |
| with: | |
| tag_name: ${{ needs.resolve.outputs.tag }} | |
| name: BrowserSkill Extension ${{ needs.resolve.outputs.version }} | |
| make_latest: false | |
| generate_release_notes: true | |
| files: dist/*.zip | |
| publish-cws: | |
| needs: [resolve, build-extension] | |
| runs-on: ubuntu-latest | |
| environment: chrome-web-store | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Download build artifact | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: extension-zip | |
| path: dist | |
| - name: Validate Chrome Web Store configuration | |
| shell: bash | |
| env: | |
| PUBLISHER_ID: ${{ vars.CHROME_WEBSTORE_PUBLISHER_ID }} | |
| EXTENSION_ID: ${{ vars.CHROME_WEBSTORE_EXTENSION_ID }} | |
| run: | | |
| set -euo pipefail | |
| if [ -z "$PUBLISHER_ID" ]; then | |
| echo "::error::Repository variable CHROME_WEBSTORE_PUBLISHER_ID is not configured" | |
| exit 1 | |
| fi | |
| if [ -z "$EXTENSION_ID" ]; then | |
| echo "::error::Repository variable CHROME_WEBSTORE_EXTENSION_ID is not configured" | |
| exit 1 | |
| fi | |
| ARCHIVE="$(find dist -maxdepth 1 -name '*.zip' -type f -print -quit)" | |
| if [ -z "$ARCHIVE" ]; then | |
| echo "::error::No extension zip found in the downloaded artifact" | |
| exit 1 | |
| fi | |
| echo "ARCHIVE=$ARCHIVE" >> "$GITHUB_ENV" | |
| - name: Authenticate with service account | |
| id: cws-auth | |
| env: | |
| SERVICE_ACCOUNT_JSON: ${{ secrets.CHROME_WEBSTORE_SERVICE_ACCOUNT_JSON }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| node --input-type=module <<'NODE' | |
| import { sign } from "node:crypto"; | |
| import { appendFileSync } from "node:fs"; | |
| const rawCredentials = process.env.SERVICE_ACCOUNT_JSON; | |
| if (!rawCredentials) { | |
| throw new Error( | |
| "Repository secret CHROME_WEBSTORE_SERVICE_ACCOUNT_JSON is not configured", | |
| ); | |
| } | |
| let credentials; | |
| try { | |
| credentials = JSON.parse(rawCredentials); | |
| } catch { | |
| throw new Error( | |
| "Repository secret CHROME_WEBSTORE_SERVICE_ACCOUNT_JSON is not valid JSON", | |
| ); | |
| } | |
| if (!credentials.client_email || !credentials.private_key) { | |
| throw new Error( | |
| "Service account JSON must contain client_email and private_key", | |
| ); | |
| } | |
| const encode = (value) => | |
| Buffer.from(JSON.stringify(value)).toString("base64url"); | |
| const issuedAt = Math.floor(Date.now() / 1000); | |
| const header = { | |
| alg: "RS256", | |
| typ: "JWT", | |
| kid: credentials.private_key_id, | |
| }; | |
| const claim = { | |
| iss: credentials.client_email, | |
| scope: "https://www.googleapis.com/auth/chromewebstore", | |
| aud: "https://oauth2.googleapis.com/token", | |
| iat: issuedAt, | |
| exp: issuedAt + 3600, | |
| }; | |
| const unsignedJwt = `${encode(header)}.${encode(claim)}`; | |
| const signature = sign( | |
| "RSA-SHA256", | |
| Buffer.from(unsignedJwt), | |
| credentials.private_key, | |
| ).toString("base64url"); | |
| const assertion = `${unsignedJwt}.${signature}`; | |
| const response = await fetch("https://oauth2.googleapis.com/token", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/x-www-form-urlencoded" }, | |
| body: new URLSearchParams({ | |
| grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer", | |
| assertion, | |
| }), | |
| }); | |
| const responseText = await response.text(); | |
| if (!response.ok) { | |
| throw new Error( | |
| `Service account token exchange failed (${response.status}): ${responseText}`, | |
| ); | |
| } | |
| const tokenResponse = JSON.parse(responseText); | |
| if (!tokenResponse.access_token) { | |
| throw new Error("Service account token response has no access_token"); | |
| } | |
| console.log(`::add-mask::${tokenResponse.access_token}`); | |
| appendFileSync( | |
| process.env.GITHUB_OUTPUT, | |
| `access_token=${tokenResponse.access_token}\n`, | |
| ); | |
| NODE | |
| - name: Upload and publish Chrome Web Store extension | |
| shell: bash | |
| env: | |
| ACCESS_TOKEN: ${{ steps.cws-auth.outputs.access_token }} | |
| PUBLISHER_ID: ${{ vars.CHROME_WEBSTORE_PUBLISHER_ID }} | |
| EXTENSION_ID: ${{ vars.CHROME_WEBSTORE_EXTENSION_ID }} | |
| run: | | |
| set -euo pipefail | |
| ITEM_NAME="publishers/${PUBLISHER_ID}/items/${EXTENSION_ID}" | |
| API_ROOT="https://chromewebstore.googleapis.com" | |
| UPLOAD_RESPONSE="$RUNNER_TEMP/cws-upload-response.json" | |
| STATUS_RESPONSE="$RUNNER_TEMP/cws-status-response.json" | |
| PUBLISH_RESPONSE="$RUNNER_TEMP/cws-publish-response.json" | |
| HTTP_STATUS="$(curl --silent --show-error \ | |
| --output "$UPLOAD_RESPONSE" \ | |
| --write-out '%{http_code}' \ | |
| --request POST \ | |
| --header "Authorization: Bearer $ACCESS_TOKEN" \ | |
| --header "Content-Type: application/zip" \ | |
| --data-binary "@$ARCHIVE" \ | |
| "$API_ROOT/upload/v2/$ITEM_NAME:upload")" | |
| if [ "$HTTP_STATUS" -lt 200 ] || [ "$HTTP_STATUS" -ge 300 ]; then | |
| echo "::error::Chrome Web Store upload failed with HTTP $HTTP_STATUS" | |
| jq . "$UPLOAD_RESPONSE" || cat "$UPLOAD_RESPONSE" | |
| exit 1 | |
| fi | |
| UPLOAD_STATE="$(jq -r '.uploadState // empty' "$UPLOAD_RESPONSE")" | |
| echo "Chrome Web Store upload state: $UPLOAD_STATE" | |
| if [ "$UPLOAD_STATE" = "IN_PROGRESS" ] || [ "$UPLOAD_STATE" = "UPLOAD_IN_PROGRESS" ]; then | |
| for attempt in $(seq 1 30); do | |
| sleep 5 | |
| HTTP_STATUS="$(curl --silent --show-error \ | |
| --output "$STATUS_RESPONSE" \ | |
| --write-out '%{http_code}' \ | |
| --header "Authorization: Bearer $ACCESS_TOKEN" \ | |
| "$API_ROOT/v2/$ITEM_NAME:fetchStatus")" | |
| if [ "$HTTP_STATUS" -lt 200 ] || [ "$HTTP_STATUS" -ge 300 ]; then | |
| echo "::error::Chrome Web Store status check failed with HTTP $HTTP_STATUS" | |
| jq . "$STATUS_RESPONSE" || cat "$STATUS_RESPONSE" | |
| exit 1 | |
| fi | |
| UPLOAD_STATE="$(jq -r '.lastAsyncUploadState // empty' "$STATUS_RESPONSE")" | |
| echo "Upload status check $attempt/30: $UPLOAD_STATE" | |
| if [ "$UPLOAD_STATE" = "SUCCEEDED" ]; then | |
| break | |
| fi | |
| if [ "$UPLOAD_STATE" != "IN_PROGRESS" ] && [ "$UPLOAD_STATE" != "UPLOAD_IN_PROGRESS" ]; then | |
| echo "::error::Chrome Web Store upload did not succeed: $UPLOAD_STATE" | |
| jq . "$STATUS_RESPONSE" | |
| exit 1 | |
| fi | |
| done | |
| fi | |
| if [ "$UPLOAD_STATE" != "SUCCEEDED" ]; then | |
| echo "::error::Chrome Web Store upload did not complete: $UPLOAD_STATE" | |
| jq . "$UPLOAD_RESPONSE" | |
| exit 1 | |
| fi | |
| HTTP_STATUS="$(curl --silent --show-error \ | |
| --output "$PUBLISH_RESPONSE" \ | |
| --write-out '%{http_code}' \ | |
| --request POST \ | |
| --header "Authorization: Bearer $ACCESS_TOKEN" \ | |
| --header "Content-Type: application/json" \ | |
| --data '{"publishType":"DEFAULT_PUBLISH"}' \ | |
| "$API_ROOT/v2/$ITEM_NAME:publish")" | |
| if [ "$HTTP_STATUS" -lt 200 ] || [ "$HTTP_STATUS" -ge 300 ]; then | |
| echo "::error::Chrome Web Store publish failed with HTTP $HTTP_STATUS" | |
| jq . "$PUBLISH_RESPONSE" || cat "$PUBLISH_RESPONSE" | |
| exit 1 | |
| fi | |
| echo "Chrome Web Store submission created successfully:" | |
| jq '{itemId, state, warningInfo}' "$PUBLISH_RESPONSE" |