Chrome Web Store #24
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: Chrome Web Store | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| version: | |
| description: Version to publish | |
| required: false | |
| type: string | |
| release: | |
| types: [published] | |
| jobs: | |
| deploy: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Get version | |
| id: version | |
| env: | |
| INPUT_VERSION: ${{ github.event.inputs.version }} | |
| run: | | |
| if [ "${{ github.event_name }}" = "release" ]; then | |
| TAG="${{ github.event.release.tag_name }}" | |
| echo "VERSION=${TAG#v}" >> $GITHUB_OUTPUT | |
| elif [ -n "$INPUT_VERSION" ]; then | |
| echo "VERSION=$INPUT_VERSION" >> $GITHUB_OUTPUT | |
| else | |
| V=$(node -p "require('./src/manifest.json').version") | |
| echo "VERSION=$V" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: "20" | |
| cache: "npm" | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Install Chromium dependencies (Puppeteer on Linux) | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y libnss3 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 libxfixes3 libxrandr2 libgbm1 libasound2t64 | |
| - name: Run E2E tests | |
| run: npm test | |
| - name: Package extension | |
| run: | | |
| cd src | |
| zip -r ../linkslinger.zip . -x "*.git*" | |
| - name: Publish to Chrome Web Store | |
| env: | |
| CHROME_EXTENSION_ID: ${{ secrets.CHROME_EXTENSION_ID }} | |
| CHROME_PUBLISHER_ID: ${{ secrets.CHROME_PUBLISHER_ID }} | |
| CHROME_CLIENT_ID: ${{ secrets.CHROME_CLIENT_ID }} | |
| CHROME_CLIENT_SECRET: ${{ secrets.CHROME_CLIENT_SECRET }} | |
| CHROME_REFRESH_TOKEN: ${{ secrets.CHROME_REFRESH_TOKEN }} | |
| run: | | |
| node <<'NODE' | |
| const fs = require("node:fs"); | |
| function required(name) { | |
| const value = process.env[name]; | |
| if (!value) throw new Error(`${name} is required`); | |
| return value; | |
| } | |
| async function requestJson(label, url, options) { | |
| const response = await fetch(url, options); | |
| const text = await response.text(); | |
| let body = {}; | |
| if (text) { | |
| try { | |
| body = JSON.parse(text); | |
| } catch (error) { | |
| throw new Error(`${label} returned non-JSON response (${response.status}): ${text}`); | |
| } | |
| } | |
| if (!response.ok) { | |
| throw new Error(`${label} failed (${response.status}): ${JSON.stringify(body)}`); | |
| } | |
| return body; | |
| } | |
| async function main() { | |
| const extensionId = required("CHROME_EXTENSION_ID"); | |
| const clientId = required("CHROME_CLIENT_ID"); | |
| const clientSecret = required("CHROME_CLIENT_SECRET"); | |
| const refreshToken = required("CHROME_REFRESH_TOKEN"); | |
| const publisherId = process.env.CHROME_PUBLISHER_ID || ""; | |
| const token = await requestJson("Token refresh", "https://oauth2.googleapis.com/token", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/x-www-form-urlencoded" }, | |
| body: new URLSearchParams({ | |
| client_id: clientId, | |
| client_secret: clientSecret, | |
| refresh_token: refreshToken, | |
| grant_type: "refresh_token" | |
| }) | |
| }); | |
| if (!token.access_token) throw new Error("Token refresh did not return an access token"); | |
| const zip = fs.readFileSync("linkslinger.zip"); | |
| const authHeaders = { Authorization: `Bearer ${token.access_token}` }; | |
| const itemName = publisherId ? `publishers/${publisherId}/items/${extensionId}` : ""; | |
| const uploadUrl = publisherId | |
| ? `https://chromewebstore.googleapis.com/upload/v2/${itemName}:upload` | |
| : `https://www.googleapis.com/upload/chromewebstore/v1.1/items/${extensionId}?uploadType=media`; | |
| const publishUrl = publisherId | |
| ? `https://chromewebstore.googleapis.com/v2/${itemName}:publish` | |
| : `https://www.googleapis.com/chromewebstore/v1.1/items/${extensionId}/publish`; | |
| const upload = await requestJson("Upload", uploadUrl, { | |
| method: publisherId ? "POST" : "PUT", | |
| headers: { ...authHeaders, "Content-Type": "application/zip" }, | |
| body: zip | |
| }); | |
| console.log(`Upload state: ${upload.uploadState || "unknown"}`); | |
| if (upload.uploadState && !["SUCCESS", "UPLOAD_SUCCESS"].includes(upload.uploadState)) { | |
| throw new Error(`Upload did not succeed: ${JSON.stringify(upload)}`); | |
| } | |
| const uploadErrors = upload.itemError || upload.itemErrors || []; | |
| if (uploadErrors.length) { | |
| throw new Error(`Upload returned item errors: ${JSON.stringify(uploadErrors)}`); | |
| } | |
| const publish = await requestJson("Publish", publishUrl, { | |
| method: "POST", | |
| headers: { ...authHeaders, "Content-Type": "application/json" }, | |
| body: publisherId ? JSON.stringify({ publishType: "DEFAULT_PUBLISH" }) : undefined | |
| }); | |
| console.log(`Publish response: ${JSON.stringify(publish)}`); | |
| if (Array.isArray(publish.status) && !publish.status.includes("OK")) { | |
| throw new Error(`Publish did not return OK status: ${JSON.stringify(publish)}`); | |
| } | |
| if (typeof publish.status === "string" && publish.status !== "OK") { | |
| throw new Error(`Publish did not return OK status: ${JSON.stringify(publish)}`); | |
| } | |
| } | |
| main().catch((error) => { | |
| console.error(error); | |
| process.exit(1); | |
| }); | |
| NODE |