Skip to content
Draft
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
36 changes: 36 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ permissions:

jobs:
release:
outputs:
version: ${{ steps.get_version.outputs.version }}
runs-on: ubuntu-latest
strategy:
matrix:
Expand Down Expand Up @@ -90,3 +92,37 @@ jobs:
${{ github.workspace }}/apps/chrome-extension/extension_output/**/*.zip
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

publish_chrome_web_store:
needs: release
if: ${{ needs.release.result == 'success' && !contains(needs.release.outputs.version, 'beta') && !contains(needs.release.outputs.version, 'alpha') && !contains(needs.release.outputs.version, 'rc') }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
ref: ${{ github.event.inputs.branch }}
- name: Download packaged Chrome extension artifact
uses: actions/download-artifact@v4
with:
name: chrome_extension
path: ${{ github.workspace }}/apps/chrome-extension/extension_output
- name: Publish Chrome extension to Chrome Web Store
id: publish_extension
continue-on-error: true
run: |
bash ./scripts/publish-chrome-extension.sh \
--zip-path "${{ github.workspace }}/apps/chrome-extension/extension_output/midscene-extension-${{ needs.release.outputs.version }}.zip"
env:
CHROME_WEB_STORE_PUBLISHER_ID: ${{ secrets.CHROME_WEB_STORE_PUBLISHER_ID }}
CHROME_WEB_STORE_CLIENT_ID: ${{ secrets.CHROME_WEB_STORE_CLIENT_ID }}
CHROME_WEB_STORE_CLIENT_SECRET: ${{ secrets.CHROME_WEB_STORE_CLIENT_SECRET }}
CHROME_WEB_STORE_REFRESH_TOKEN: ${{ secrets.CHROME_WEB_STORE_REFRESH_TOKEN }}
- name: Summarize manual fallback when Chrome Web Store publish fails
if: ${{ steps.publish_extension.outcome == 'failure' }}
run: |
echo "## Chrome Web Store publish fallback" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Chrome Web Store publishing failed, but the GitHub Release and packaged extension artifact were created successfully." >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "You can manually upload:" >> $GITHUB_STEP_SUMMARY
echo "\`${{ github.workspace }}/apps/chrome-extension/extension_output/midscene-extension-${{ needs.release.outputs.version }}.zip\`" >> $GITHUB_STEP_SUMMARY
13 changes: 12 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,15 @@ Here are the steps to publish (we generally use CI for releases and avoid publis
1. [Run the release action](https://github.com/web-infra-dev/midscene/actions/workflows/release.yml).
2. [Generate the release notes](https://github.com/web-infra-dev/midscene/releases).

Stable releases also attempt to submit the packaged Chrome extension to the Chrome Web Store from CI. Configure these repository secrets before running a stable release:

- `CHROME_WEB_STORE_PUBLISHER_ID`
- `CHROME_WEB_STORE_CLIENT_ID`
- `CHROME_WEB_STORE_CLIENT_SECRET`
- `CHROME_WEB_STORE_REFRESH_TOKEN`

If Chrome Web Store publishing fails, the GitHub Release and packaged extension zip are still generated. You can then download the zip from the release artifacts and upload it manually in the Chrome Web Store dashboard.

## Chrome Extension

### Directory Structure
Expand All @@ -341,7 +350,7 @@ midscene/
│ ├── core/ # Core functionality
│ ├── visualizer/ # Visualization components
│ ├── web-integration/ # Web integration
│ └── ...
└── ...
└── ...
```

Expand Down Expand Up @@ -382,6 +391,8 @@ The built `dist` directory can be directly installed as a Chrome extension. In C
Alternatively, you can use the packaged extension:
- Select the `apps/chrome-extension/extension_output/midscene-extension-v{version}.zip` file

For stable releases, this packaged zip is also the artifact used by the Chrome Web Store publish job. If that job fails, you can still use the same zip for manual store upload.

For more detailed information, please refer to [Chrome DevTools README](./apps/chrome-extension/README.md).


Expand Down
273 changes: 273 additions & 0 deletions scripts/publish-chrome-extension.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,273 @@
#!/usr/bin/env bash

set -euo pipefail

readonly DEFAULT_ITEM_ID="gbldofcpkknbggpkmbdaefngejllnief"
readonly DEFAULT_TIMEOUT_SECONDS=180
readonly DEFAULT_POLL_INTERVAL_SECONDS=10

ZIP_PATH=""
ITEM_ID="$DEFAULT_ITEM_ID"
TIMEOUT_SECONDS="${CHROME_WEB_STORE_TIMEOUT_SECONDS:-$DEFAULT_TIMEOUT_SECONDS}"
POLL_INTERVAL_SECONDS="${CHROME_WEB_STORE_POLL_INTERVAL_SECONDS:-$DEFAULT_POLL_INTERVAL_SECONDS}"
VERBOSE=0

usage() {
cat <<'EOF'
Usage:
bash scripts/publish-chrome-extension.sh --zip-path <path-to-zip> [--verbose]

Required environment variables:
CHROME_WEB_STORE_PUBLISHER_ID
CHROME_WEB_STORE_CLIENT_ID
CHROME_WEB_STORE_CLIENT_SECRET
CHROME_WEB_STORE_REFRESH_TOKEN

Notes:
- This script targets the fixed Midscene Chrome Web Store item.
- It uploads the packaged zip, submits it for publishing, and waits until
the submission reaches a review or published state.
EOF
}

log() {
printf '%s\n' "$*"
}

debug() {
if [[ "$VERBOSE" -eq 1 ]]; then
printf '[debug] %s\n' "$*"
fi
}

fail() {
printf 'Error: %s\n' "$*" >&2
exit 1
}

require_command() {
command -v "$1" >/dev/null 2>&1 || fail "Missing required command: $1"
}

require_env() {
local name="$1"
if [[ -z "${!name:-}" ]]; then
fail "Missing required environment variable: $name"
fi
}

append_summary() {
if [[ -n "${GITHUB_STEP_SUMMARY:-}" ]]; then
printf '%s\n' "$*" >>"$GITHUB_STEP_SUMMARY"
fi
}

while [[ $# -gt 0 ]]; do
case "$1" in
--zip-path)
ZIP_PATH="${2:-}"
shift 2
;;
--timeout-seconds)
TIMEOUT_SECONDS="${2:-}"
shift 2
;;
--poll-interval-seconds)
POLL_INTERVAL_SECONDS="${2:-}"
shift 2
;;
--verbose)
VERBOSE=1
shift
;;
--help|-h)
usage
exit 0
;;
*)
fail "Unknown argument: $1"
;;
esac
done

if [[ -z "$ZIP_PATH" ]]; then
usage
fail "--zip-path is required"
fi

require_command curl
require_command jq
require_command unzip

require_env CHROME_WEB_STORE_PUBLISHER_ID
require_env CHROME_WEB_STORE_CLIENT_ID
require_env CHROME_WEB_STORE_CLIENT_SECRET
require_env CHROME_WEB_STORE_REFRESH_TOKEN

[[ -f "$ZIP_PATH" ]] || fail "Zip file does not exist: $ZIP_PATH"
[[ "$TIMEOUT_SECONDS" =~ ^[0-9]+$ ]] || fail "--timeout-seconds must be numeric"
[[ "$POLL_INTERVAL_SECONDS" =~ ^[0-9]+$ ]] || fail "--poll-interval-seconds must be numeric"
(( TIMEOUT_SECONDS > 0 )) || fail "--timeout-seconds must be greater than zero"
(( POLL_INTERVAL_SECONDS > 0 )) || fail "--poll-interval-seconds must be greater than zero"

readonly ITEM_NAME="publishers/${CHROME_WEB_STORE_PUBLISHER_ID}/items/${ITEM_ID}"
readonly STATUS_URL="https://chromewebstore.googleapis.com/v2/${ITEM_NAME}:fetchStatus"
readonly UPLOAD_URL="https://chromewebstore.googleapis.com/upload/v2/${ITEM_NAME}:upload"
readonly PUBLISH_URL="https://chromewebstore.googleapis.com/v2/${ITEM_NAME}:publish"

EXPECTED_CRX_VERSION="$(unzip -p "$ZIP_PATH" manifest.json | jq -er '.version')"

debug "Target item: $ITEM_NAME"
debug "Zip path: $ZIP_PATH"
debug "Expected extension version from manifest: $EXPECTED_CRX_VERSION"

ACCESS_TOKEN="$(
curl --silent --show-error --fail-with-body \
-X POST "https://oauth2.googleapis.com/token" \
-d "client_id=${CHROME_WEB_STORE_CLIENT_ID}" \
-d "client_secret=${CHROME_WEB_STORE_CLIENT_SECRET}" \
-d "refresh_token=${CHROME_WEB_STORE_REFRESH_TOKEN}" \
-d "grant_type=refresh_token" \
| jq -er '.access_token'
)"

api_get() {
local url="$1"
curl --silent --show-error --fail-with-body \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
"$url"
}

api_post_json() {
local url="$1"
local body="$2"
curl --silent --show-error --fail-with-body \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "Content-Type: application/json" \
-X POST \
-d "$body" \
"$url"
}

wait_for_upload() {
local deadline=$((SECONDS + TIMEOUT_SECONDS))
local status_json=""
local upload_state=""

while (( SECONDS < deadline )); do
status_json="$(api_get "$STATUS_URL")"
upload_state="$(jq -r '.lastAsyncUploadState // "NOT_FOUND"' <<<"$status_json")"
debug "Current upload state: $upload_state"

case "$upload_state" in
SUCCEEDED)
return 0
;;
IN_PROGRESS|NOT_FOUND)
sleep "$POLL_INTERVAL_SECONDS"
;;
FAILED)
fail "Chrome Web Store upload failed: $status_json"
;;
*)
fail "Unexpected upload state: $upload_state"
;;
esac
done

fail "Timed out waiting for Chrome Web Store upload to finish"
}

wait_for_publish_state() {
local deadline=$((SECONDS + TIMEOUT_SECONDS))
local status_json=""
local submitted_state=""
local submitted_version=""
local published_state=""
local published_version=""

while (( SECONDS < deadline )); do
status_json="$(api_get "$STATUS_URL")"
submitted_state="$(jq -r '.submittedItemRevisionStatus.state // empty' <<<"$status_json")"
submitted_version="$(jq -r '.submittedItemRevisionStatus.distributionChannels[0].crxVersion // empty' <<<"$status_json")"
published_state="$(jq -r '.publishedItemRevisionStatus.state // empty' <<<"$status_json")"
published_version="$(jq -r '.publishedItemRevisionStatus.distributionChannels[0].crxVersion // empty' <<<"$status_json")"

debug "Submitted version/state: ${submitted_version:-<none>} / ${submitted_state:-<none>}"
debug "Published version/state: ${published_version:-<none>} / ${published_state:-<none>}"

if [[ "$submitted_version" == "$EXPECTED_CRX_VERSION" ]]; then
case "$submitted_state" in
PENDING_REVIEW|STAGED)
log "Chrome Web Store submission accepted: version ${submitted_version}, state ${submitted_state}"
append_summary "### Chrome Web Store"
append_summary "- Item: \`${ITEM_ID}\`"
append_summary "- Uploaded version: \`${EXPECTED_CRX_VERSION}\`"
append_summary "- Submission state: \`${submitted_state}\`"
return 0
;;
REJECTED|CANCELLED)
fail "Chrome Web Store submission did not succeed: $status_json"
;;
esac
fi

if [[ "$published_version" == "$EXPECTED_CRX_VERSION" ]]; then
case "$published_state" in
PUBLISHED|PUBLISHED_TO_TESTERS)
log "Chrome Web Store publish complete: version ${published_version}, state ${published_state}"
append_summary "### Chrome Web Store"
append_summary "- Item: \`${ITEM_ID}\`"
append_summary "- Published version: \`${published_version}\`"
append_summary "- Published state: \`${published_state}\`"
return 0
;;
esac
fi

sleep "$POLL_INTERVAL_SECONDS"
done

fail "Timed out waiting for Chrome Web Store submission to reach a review or published state"
}

log "Uploading ${ZIP_PATH} to Chrome Web Store item ${ITEM_ID}"
UPLOAD_RESPONSE="$(
curl --silent --show-error --fail-with-body \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-X POST \
-T "$ZIP_PATH" \
"$UPLOAD_URL"
)"

UPLOAD_STATE="$(jq -er '.uploadState' <<<"$UPLOAD_RESPONSE")"
UPLOADED_CRX_VERSION="$(jq -r '.crxVersion // empty' <<<"$UPLOAD_RESPONSE")"

if [[ -n "$UPLOADED_CRX_VERSION" ]]; then
EXPECTED_CRX_VERSION="$UPLOADED_CRX_VERSION"
fi

log "Upload state: ${UPLOAD_STATE}"
debug "Upload response: $UPLOAD_RESPONSE"

case "$UPLOAD_STATE" in
SUCCEEDED)
;;
IN_PROGRESS)
wait_for_upload
;;
*)
fail "Chrome Web Store upload failed: $UPLOAD_RESPONSE"
;;
esac

log "Submitting uploaded package for publishing"
PUBLISH_RESPONSE="$(api_post_json "$PUBLISH_URL" '{}')"
PUBLISH_STATE="$(jq -r '.state // empty' <<<"$PUBLISH_RESPONSE")"

debug "Publish response: $PUBLISH_RESPONSE"
if [[ "$PUBLISH_STATE" == "REJECTED" || "$PUBLISH_STATE" == "CANCELLED" ]]; then
fail "Chrome Web Store publish request failed: $PUBLISH_RESPONSE"
fi

wait_for_publish_state
Loading