Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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/
Expand Down
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
31 changes: 31 additions & 0 deletions app.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ usage() {
Usage:
./app.sh --build
./app.sh --release
./app.sh --publish-cws
./app.sh --release-cws
EOF
}

Expand Down Expand Up @@ -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}"
Expand Down Expand Up @@ -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
Expand Down
22 changes: 22 additions & 0 deletions docs/release.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`

Expand Down
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
16 changes: 16 additions & 0 deletions scripts/release/chrome-web-store.env.example
Original file line number Diff line number Diff line change
@@ -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
163 changes: 163 additions & 0 deletions scripts/release/chrome-web-store.publish.sh.template
Original file line number Diff line number Diff line change
@@ -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 <x.y.z>
./scripts/release/chrome-web-store.publish.sh --zip <path-to-cws-zip>

Options:
--version <x.y.z> Version used to resolve default zip path.
--zip <path> Explicit path to onui-chrome-web-store zip.
--env-file <path> 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"
27 changes: 27 additions & 0 deletions scripts/release/init-cws-publish-local.sh
Original file line number Diff line number Diff line change
@@ -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"
Loading