From d9fa048e868c9a0719ba8e2cdc0a5533edc44a24 Mon Sep 17 00:00:00 2001 From: Tushar Shukla Date: Sun, 22 Feb 2026 01:15:01 +0100 Subject: [PATCH 1/3] feat(release): add local automated Chrome Web Store publish workflow --- .gitignore | 2 + README.md | 22 +++ app.sh | 31 ++++ docs/release.md | 22 +++ scripts/release/chrome-web-store.env.example | 16 ++ .../chrome-web-store.publish.sh.template | 163 ++++++++++++++++++ scripts/release/init-cws-publish-local.sh | 27 +++ 7 files changed, 283 insertions(+) create mode 100644 scripts/release/chrome-web-store.env.example create mode 100644 scripts/release/chrome-web-store.publish.sh.template create mode 100755 scripts/release/init-cws-publish-local.sh diff --git a/.gitignore b/.gitignore index 46fdbd6..69ddf7c 100644 --- a/.gitignore +++ b/.gitignore @@ -42,6 +42,8 @@ Thumbs.db .env .env.local .env.*.local +scripts/release/chrome-web-store.publish.sh +scripts/release/chrome-web-store.env # Test coverage coverage/ diff --git a/README.md b/README.md index 303f11d..a6d5d22 100644 --- a/README.md +++ b/README.md @@ -197,6 +197,28 @@ Release actions: 5. Push commit/tag 6. Create GitHub release with packaged assets +### Local Chrome Web Store publish automation + +Create local publish files (gitignored): + +```bash +./scripts/release/init-cws-publish-local.sh +``` + +Then set credentials in `scripts/release/chrome-web-store.env`. + +Publish current version to CWS: + +```bash +./app.sh --publish-cws +``` + +Release + CWS publish in one run: + +```bash +./app.sh --release-cws +``` + ## 🛠️ Development ```bash diff --git a/app.sh b/app.sh index 95956b3..cbd9aec 100755 --- a/app.sh +++ b/app.sh @@ -9,6 +9,8 @@ usage() { Usage: ./app.sh --build ./app.sh --release + ./app.sh --publish-cws + ./app.sh --release-cws EOF } @@ -178,6 +180,15 @@ package_artifacts() { ls -1 "$artifacts_dir" } +publish_chrome_web_store() { + local version="$1" + local script_path="scripts/release/chrome-web-store.publish.sh" + + [ -x "$script_path" ] || fail "Missing local executable: $script_path. Run: ./scripts/release/init-cws-publish-local.sh" + log "Publishing Chrome Web Store package for v${version}" + "$script_path" --version "$version" +} + release_to_github() { local version="$1" local tag="v${version}" @@ -241,6 +252,26 @@ main() { package_artifacts "$version" release_to_github "$version" ;; + --publish-cws) + preflight_common + local version + version="$(current_version)" + publish_chrome_web_store "$version" + ;; + --release-cws) + preflight_common + assert_clean_tree + assert_main_branch + assert_gh_auth + local version + version="$(next_patch_version)" + log "Bumping version to $version" + sync_version "$version" + run_build_pipeline + package_artifacts "$version" + release_to_github "$version" + publish_chrome_web_store "$version" + ;; *) usage exit 1 diff --git a/docs/release.md b/docs/release.md index 90f2082..9348636 100644 --- a/docs/release.md +++ b/docs/release.md @@ -42,6 +42,28 @@ This command: Upload `onui-chrome-web-store-vX.Y.Z.zip` from `artifacts/vX.Y.Z/`. The CWS zip strips the `manifest.key` field automatically. +## Automated Chrome Web Store Publish (Local Only) + +Create local files (both gitignored): + +```bash +./scripts/release/init-cws-publish-local.sh +``` + +Then edit `scripts/release/chrome-web-store.env` with your credentials and publisher ID. + +Publish current version: + +```bash +./app.sh --publish-cws +``` + +Release and then publish to CWS in one run: + +```bash +./app.sh --release-cws +``` + Live listing: `https://chromewebstore.google.com/detail/onui/hllgijkdhegkpooopdhbfdjialkhlkan` diff --git a/scripts/release/chrome-web-store.env.example b/scripts/release/chrome-web-store.env.example new file mode 100644 index 0000000..9a8e38a --- /dev/null +++ b/scripts/release/chrome-web-store.env.example @@ -0,0 +1,16 @@ +# Chrome Web Store API credentials (local only) +# Copy this file to: +# scripts/release/chrome-web-store.env + +# Extension ID from Chrome Web Store listing +CWS_EXTENSION_ID=hllgijkdhegkpooopdhbfdjialkhlkan + +# Publisher ID from your Chrome Web Store developer account +CWS_PUBLISHER_ID=replace-with-publisher-id + +# OAuth client credentials from Google Cloud project +CWS_CLIENT_ID=replace-with-client-id.apps.googleusercontent.com +CWS_CLIENT_SECRET=replace-with-client-secret + +# OAuth refresh token with Chrome Web Store publish scope +CWS_REFRESH_TOKEN=replace-with-refresh-token diff --git a/scripts/release/chrome-web-store.publish.sh.template b/scripts/release/chrome-web-store.publish.sh.template new file mode 100644 index 0000000..0dc8f6d --- /dev/null +++ b/scripts/release/chrome-web-store.publish.sh.template @@ -0,0 +1,163 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +DEFAULT_ENV_FILE="$ROOT_DIR/scripts/release/chrome-web-store.env" +API_BASE="https://chromewebstore.googleapis.com" +UPLOAD_BASE="https://chromewebstore.googleapis.com/upload" + +usage() { + cat <<'EOF' +Usage: + ./scripts/release/chrome-web-store.publish.sh --version + ./scripts/release/chrome-web-store.publish.sh --zip + +Options: + --version Version used to resolve default zip path. + --zip Explicit path to onui-chrome-web-store zip. + --env-file Override local env file path. + --skip-publish Upload only (do not publish). +EOF +} + +require_cmd() { + command -v "$1" >/dev/null 2>&1 || { + printf '[cws] ERROR: Missing command: %s\n' "$1" >&2 + exit 1 + } +} + +fail() { + printf '[cws] ERROR: %s\n' "$1" >&2 + exit 1 +} + +log() { + printf '[cws] %s\n' "$1" +} + +version="" +zip_path="" +env_file="$DEFAULT_ENV_FILE" +skip_publish="false" + +while [ "$#" -gt 0 ]; do + case "$1" in + --version) + [ "$#" -ge 2 ] || fail "Missing value for --version" + version="$2" + shift 2 + ;; + --zip) + [ "$#" -ge 2 ] || fail "Missing value for --zip" + zip_path="$2" + shift 2 + ;; + --env-file) + [ "$#" -ge 2 ] || fail "Missing value for --env-file" + env_file="$2" + shift 2 + ;; + --skip-publish) + skip_publish="true" + shift + ;; + -h|--help) + usage + exit 0 + ;; + *) + fail "Unknown argument: $1" + ;; + esac +done + +require_cmd curl +require_cmd node + +[ -f "$env_file" ] || fail "Missing env file: $env_file" +# shellcheck disable=SC1090 +source "$env_file" + +extension_id="${CWS_EXTENSION_ID:-hllgijkdhegkpooopdhbfdjialkhlkan}" +publisher_id="${CWS_PUBLISHER_ID:-}" +client_id="${CWS_CLIENT_ID:-}" +client_secret="${CWS_CLIENT_SECRET:-}" +refresh_token="${CWS_REFRESH_TOKEN:-}" + +[ -n "$publisher_id" ] || fail "CWS_PUBLISHER_ID is required" +[ -n "$client_id" ] || fail "CWS_CLIENT_ID is required" +[ -n "$client_secret" ] || fail "CWS_CLIENT_SECRET is required" +[ -n "$refresh_token" ] || fail "CWS_REFRESH_TOKEN is required" + +if [ -z "$version" ] && [ -z "$zip_path" ]; then + version="$(node -p "require('$ROOT_DIR/package.json').version")" +fi + +if [ -z "$zip_path" ]; then + zip_path="$ROOT_DIR/artifacts/v${version}/onui-chrome-web-store-v${version}.zip" +fi + +[ -f "$zip_path" ] || fail "Zip not found: $zip_path" + +log "Requesting OAuth access token" +token_response="$(curl -fsS -X POST 'https://oauth2.googleapis.com/token' \ + -H 'Content-Type: application/x-www-form-urlencoded' \ + --data-urlencode "client_id=$client_id" \ + --data-urlencode "client_secret=$client_secret" \ + --data-urlencode "refresh_token=$refresh_token" \ + --data-urlencode 'grant_type=refresh_token')" + +access_token="$(printf '%s' "$token_response" | node -e " +const fs = require('node:fs'); +const raw = fs.readFileSync(0, 'utf8'); +const data = JSON.parse(raw); +if (!data.access_token) { + console.error(data.error_description || data.error || 'Could not fetch access token'); + process.exit(1); +} +process.stdout.write(data.access_token); +")" + +upload_url="$UPLOAD_BASE/v2/publishers/$publisher_id/items/$extension_id:upload" +publish_url="$API_BASE/v2/publishers/$publisher_id/items/$extension_id:publish" +status_url="$API_BASE/v2/publishers/$publisher_id/items/$extension_id:fetchStatus" + +log "Uploading zip to Chrome Web Store API (v2)" +upload_response="$(curl -fsS -X POST "$upload_url" \ + -H "Authorization: Bearer $access_token" \ + -H 'Content-Type: application/zip' \ + --data-binary "@$zip_path")" +printf '%s' "$upload_response" | node -e " +const fs = require('node:fs'); +const raw = fs.readFileSync(0, 'utf8'); +const data = JSON.parse(raw); +console.log('[cws] upload response:', JSON.stringify(data)); +" + +if [ "$skip_publish" = "true" ]; then + log "Skipping publish as requested (--skip-publish)" + exit 0 +fi + +log "Publishing extension" +publish_response="$(curl -fsS -X POST "$publish_url" \ + -H "Authorization: Bearer $access_token")" +printf '%s' "$publish_response" | node -e " +const fs = require('node:fs'); +const raw = fs.readFileSync(0, 'utf8'); +const data = JSON.parse(raw); +console.log('[cws] publish response:', JSON.stringify(data)); +" + +log "Fetching status" +status_response="$(curl -fsS -X GET "$status_url" \ + -H "Authorization: Bearer $access_token")" +printf '%s' "$status_response" | node -e " +const fs = require('node:fs'); +const raw = fs.readFileSync(0, 'utf8'); +const data = JSON.parse(raw); +console.log('[cws] status response:', JSON.stringify(data)); +" + +log "Done" diff --git a/scripts/release/init-cws-publish-local.sh b/scripts/release/init-cws-publish-local.sh new file mode 100755 index 0000000..ca568db --- /dev/null +++ b/scripts/release/init-cws-publish-local.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +LOCAL_SCRIPT="$ROOT_DIR/scripts/release/chrome-web-store.publish.sh" +LOCAL_ENV="$ROOT_DIR/scripts/release/chrome-web-store.env" +TEMPLATE_SCRIPT="$ROOT_DIR/scripts/release/chrome-web-store.publish.sh.template" +TEMPLATE_ENV="$ROOT_DIR/scripts/release/chrome-web-store.env.example" + +printf '[cws-init] Preparing local Chrome Web Store publish files\n' + +if [ ! -f "$LOCAL_SCRIPT" ]; then + cp "$TEMPLATE_SCRIPT" "$LOCAL_SCRIPT" + chmod +x "$LOCAL_SCRIPT" + printf '[cws-init] Created %s\n' "$LOCAL_SCRIPT" +else + printf '[cws-init] Exists: %s\n' "$LOCAL_SCRIPT" +fi + +if [ ! -f "$LOCAL_ENV" ]; then + cp "$TEMPLATE_ENV" "$LOCAL_ENV" + printf '[cws-init] Created %s\n' "$LOCAL_ENV" +else + printf '[cws-init] Exists: %s\n' "$LOCAL_ENV" +fi + +printf '[cws-init] Both files are gitignored. Fill credentials in %s\n' "$LOCAL_ENV" From 43f0c0919cec05208a9ca06cbda20e709a6b0d8e Mon Sep 17 00:00:00 2001 From: Tushar Shukla Date: Sun, 22 Feb 2026 12:37:12 +0100 Subject: [PATCH 2/3] fix(ci): ensure clean-run quality checks and cws api url interpolation --- package.json | 8 ++++---- scripts/release/chrome-web-store.publish.sh.template | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 8f222ee..060b72c 100644 --- a/package.json +++ b/package.json @@ -12,11 +12,11 @@ "build": "pnpm --filter @onui/extension build", "build:all": "pnpm --filter @onui/core build && pnpm --filter @onui/mcp-server build && pnpm --filter @onui/extension build", "test": "pnpm test:all", - "test:all": "pnpm --filter @onui/mcp-server test && pnpm --filter @onui/extension test", + "test:all": "pnpm --filter @onui/core build && pnpm --filter @onui/mcp-server run test && pnpm --filter @onui/extension test", "test:extension": "pnpm --filter @onui/extension test", - "test:mcp": "pnpm --filter @onui/mcp-server test", - "test:coverage": "pnpm --filter @onui/mcp-server test:coverage && pnpm --filter @onui/extension test:coverage", - "typecheck": "pnpm --filter @onui/core typecheck && pnpm --filter @onui/mcp-server typecheck && pnpm --filter @onui/extension typecheck", + "test:mcp": "pnpm --filter @onui/core build && pnpm --filter @onui/mcp-server run test", + "test:coverage": "pnpm --filter @onui/core build && pnpm --filter @onui/mcp-server run test:coverage && pnpm --filter @onui/extension test:coverage", + "typecheck": "pnpm --filter @onui/core build && pnpm --filter @onui/core typecheck && pnpm --filter @onui/mcp-server run typecheck && pnpm --filter @onui/extension typecheck", "check": "pnpm typecheck && pnpm test:all && pnpm build:all", "build:mcp": "pnpm --filter @onui/mcp-server build", "setup:mcp": "pnpm --filter @onui/mcp-server run setup", diff --git a/scripts/release/chrome-web-store.publish.sh.template b/scripts/release/chrome-web-store.publish.sh.template index 0dc8f6d..b2b3543 100644 --- a/scripts/release/chrome-web-store.publish.sh.template +++ b/scripts/release/chrome-web-store.publish.sh.template @@ -119,9 +119,9 @@ if (!data.access_token) { process.stdout.write(data.access_token); ")" -upload_url="$UPLOAD_BASE/v2/publishers/$publisher_id/items/$extension_id:upload" -publish_url="$API_BASE/v2/publishers/$publisher_id/items/$extension_id:publish" -status_url="$API_BASE/v2/publishers/$publisher_id/items/$extension_id:fetchStatus" +upload_url="$UPLOAD_BASE/v2/publishers/${publisher_id}/items/${extension_id}:upload" +publish_url="$API_BASE/v2/publishers/${publisher_id}/items/${extension_id}:publish" +status_url="$API_BASE/v2/publishers/${publisher_id}/items/${extension_id}:fetchStatus" log "Uploading zip to Chrome Web Store API (v2)" upload_response="$(curl -fsS -X POST "$upload_url" \ From ff5e327952c76ea0b4ee03e940de32fe31fa5898 Mon Sep 17 00:00:00 2001 From: Tushar Shukla Date: Sun, 22 Feb 2026 12:37:32 +0100 Subject: [PATCH 3/3] chore(ci): keep cws publish script changes local only --- scripts/release/chrome-web-store.publish.sh.template | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/release/chrome-web-store.publish.sh.template b/scripts/release/chrome-web-store.publish.sh.template index b2b3543..0dc8f6d 100644 --- a/scripts/release/chrome-web-store.publish.sh.template +++ b/scripts/release/chrome-web-store.publish.sh.template @@ -119,9 +119,9 @@ if (!data.access_token) { process.stdout.write(data.access_token); ")" -upload_url="$UPLOAD_BASE/v2/publishers/${publisher_id}/items/${extension_id}:upload" -publish_url="$API_BASE/v2/publishers/${publisher_id}/items/${extension_id}:publish" -status_url="$API_BASE/v2/publishers/${publisher_id}/items/${extension_id}:fetchStatus" +upload_url="$UPLOAD_BASE/v2/publishers/$publisher_id/items/$extension_id:upload" +publish_url="$API_BASE/v2/publishers/$publisher_id/items/$extension_id:publish" +status_url="$API_BASE/v2/publishers/$publisher_id/items/$extension_id:fetchStatus" log "Uploading zip to Chrome Web Store API (v2)" upload_response="$(curl -fsS -X POST "$upload_url" \