diff --git a/.github/actions/cache/restore/action.yml b/.github/actions/cache/restore/action.yml new file mode 100644 index 0000000000..1ab78a0f42 --- /dev/null +++ b/.github/actions/cache/restore/action.yml @@ -0,0 +1,40 @@ +name: Restore cache + +description: Restore a cache from GitHub storage or the self-hosted internal cache service + +inputs: + key: + description: An explicit key for restoring the cache + required: true + path: + description: Files, directories, and wildcard patterns to restore + required: true + restore-keys: + description: Ordered prefixes used to restore stale caches + required: false + +outputs: + cache-hit: + description: Whether the primary cache key was restored + value: ${{ steps.github-cache.outputs.cache-hit == 'true' || steps.internal-cache.outputs.cache-hit == 'true' }} + +runs: + using: composite + steps: + - name: Restore cache from GitHub storage + id: github-cache + if: ${{ runner.environment == 'github-hosted' }} + uses: actions/cache/restore@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6 + with: + key: ${{ inputs.key }} + path: ${{ inputs.path }} + restore-keys: ${{ inputs.restore-keys }} + + - name: Restore cache from internal storage + id: internal-cache + if: ${{ runner.environment == 'self-hosted' }} + uses: lynx-infra/cache/restore@d6bd70bf2182b228ff5cfa9539e5f61b0929a1ba # main + with: + key: ${{ inputs.key }} + path: ${{ inputs.path }} + restore-keys: ${{ inputs.restore-keys }} diff --git a/.github/actions/cache/save/action.yml b/.github/actions/cache/save/action.yml new file mode 100644 index 0000000000..a19209ec26 --- /dev/null +++ b/.github/actions/cache/save/action.yml @@ -0,0 +1,28 @@ +name: Save cache + +description: Save a cache to GitHub storage or the self-hosted internal cache service + +inputs: + key: + description: An explicit key for saving the cache + required: true + path: + description: Files, directories, and wildcard patterns to save + required: true + +runs: + using: composite + steps: + - name: Save cache to GitHub storage + if: ${{ runner.environment == 'github-hosted' }} + uses: actions/cache/save@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6 + with: + key: ${{ inputs.key }} + path: ${{ inputs.path }} + + - name: Save cache to internal storage + if: ${{ runner.environment == 'self-hosted' }} + uses: lynx-infra/cache/save@d6bd70bf2182b228ff5cfa9539e5f61b0929a1ba # main + with: + key: ${{ inputs.key }} + path: ${{ inputs.path }} diff --git a/.github/actions/pnpm-cache/restore/action.yml b/.github/actions/pnpm-cache/restore/action.yml new file mode 100644 index 0000000000..72eed2259e --- /dev/null +++ b/.github/actions/pnpm-cache/restore/action.yml @@ -0,0 +1,27 @@ +name: Restore pnpm store + +description: Restore the pnpm content-addressable store from the runner-appropriate backend + +outputs: + cache-hit: + description: Whether the primary pnpm cache key was restored + value: ${{ steps.cache.outputs.cache-hit }} + +runs: + using: composite + steps: + - name: Resolve pnpm store + id: pnpm-store + shell: bash + run: | + echo "path=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT" + echo "version=$(pnpm --version)" >> "$GITHUB_OUTPUT" + + - name: Restore pnpm store + id: cache + uses: ./.github/actions/cache/restore + with: + path: ${{ steps.pnpm-store.outputs.path }} + key: midscene-pnpm-v1-${{ runner.os }}-${{ runner.arch }}-${{ steps.pnpm-store.outputs.version }}-${{ hashFiles('pnpm-lock.yaml') }} + restore-keys: | + midscene-pnpm-v1-${{ runner.os }}-${{ runner.arch }}-${{ steps.pnpm-store.outputs.version }}- diff --git a/.github/actions/pnpm-cache/save/action.yml b/.github/actions/pnpm-cache/save/action.yml new file mode 100644 index 0000000000..9dbf0058a3 --- /dev/null +++ b/.github/actions/pnpm-cache/save/action.yml @@ -0,0 +1,19 @@ +name: Save pnpm store + +description: Save the pnpm content-addressable store to the runner-appropriate backend + +runs: + using: composite + steps: + - name: Resolve pnpm store + id: pnpm-store + shell: bash + run: | + echo "path=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT" + echo "version=$(pnpm --version)" >> "$GITHUB_OUTPUT" + + - name: Save pnpm store + uses: ./.github/actions/cache/save + with: + path: ${{ steps.pnpm-store.outputs.path }} + key: midscene-pnpm-v1-${{ runner.os }}-${{ runner.arch }}-${{ steps.pnpm-store.outputs.version }}-${{ hashFiles('pnpm-lock.yaml') }} diff --git a/.github/actions/setup-headless-browser/action.yml b/.github/actions/setup-headless-browser/action.yml new file mode 100644 index 0000000000..e425e7a70c --- /dev/null +++ b/.github/actions/setup-headless-browser/action.yml @@ -0,0 +1,80 @@ +name: Setup headless browser + +description: Install missing Linux desktop libraries and restore a Puppeteer Chrome binary + +inputs: + package-directory: + description: Workspace package that provides the Puppeteer CLI + required: false + default: packages/web-integration + +outputs: + browser-path: + description: Absolute path to the Chrome for Testing executable + value: ${{ steps.browser.outputs.browser-path }} + browser-cache-hit: + description: Whether the Puppeteer browser cache was restored + value: ${{ steps.browser.outputs.browser-cache-hit }} + +runs: + using: composite + steps: + - name: Install missing system dependencies + shell: bash + run: | + set -euo pipefail + + packages=( + xvfb + x11-xserver-utils + imagemagick + fonts-liberation + libasound2 + libatk-bridge2.0-0 + libdrm2 + libgbm1 + libgtk-3-0 + libnss3 + libpango-1.0-0 + libvulkan1 + libx11-6 + libx11-xcb1 + libxcomposite1 + libxdamage1 + libxfixes3 + libxinerama1 + libxkbcommon-x11-0 + libxkbcommon0 + libxrandr2 + libxshmfence1 + libxss1 + libxtst6 + xdg-utils + ) + missing=() + + for package in "${packages[@]}"; do + status="$(dpkg-query -W -f='${Status}' "$package" 2>/dev/null || true)" + if [ "$status" != "install ok installed" ]; then + missing+=("$package") + fi + done + + if [ "${#missing[@]}" -eq 0 ]; then + echo "All headless browser system dependencies are preinstalled" + exit 0 + fi + + echo "Installing missing packages: ${missing[*]}" + sudo apt-get update + sudo env DEBIAN_FRONTEND=noninteractive apt-get install \ + --yes \ + --no-install-recommends \ + "${missing[@]}" + + - name: Setup Puppeteer browser + id: browser + uses: ./.github/actions/setup-puppeteer-browser + with: + package-directory: ${{ inputs.package-directory }} + expose-as-chromium: "true" diff --git a/.github/actions/setup-node-pnpm/action.yml b/.github/actions/setup-node-pnpm/action.yml new file mode 100644 index 0000000000..f9b9f3d84d --- /dev/null +++ b/.github/actions/setup-node-pnpm/action.yml @@ -0,0 +1,57 @@ +name: Setup Node.js and pnpm + +description: Restore the Node.js toolcache on self-hosted runners, then set up Node.js and pnpm + +inputs: + node-version: + description: Node.js version to install + required: false + default: 24.13.0 + pnpm-version: + description: pnpm version to install + required: false + default: 9.3.0 + +outputs: + node-cache-hit: + description: Whether the self-hosted Node.js toolcache was restored + value: ${{ steps.node-cache.outputs.cache-hit }} + +runs: + using: composite + steps: + - name: Resolve Node.js toolcache + id: node-toolcache + if: ${{ runner.environment == 'self-hosted' }} + shell: bash + run: | + if [ -z "${RUNNER_TOOL_CACHE:-}" ]; then + echo "::error::RUNNER_TOOL_CACHE is not configured" + exit 1 + fi + echo "path=$RUNNER_TOOL_CACHE/node/${{ inputs.node-version }}" >> "$GITHUB_OUTPUT" + + - name: Restore Node.js toolcache + id: node-cache + if: ${{ runner.environment == 'self-hosted' }} + uses: ./.github/actions/cache/restore + with: + path: ${{ steps.node-toolcache.outputs.path }} + key: midscene-node-toolcache-v1-${{ runner.os }}-${{ runner.arch }}-${{ inputs.node-version }} + + - name: Setup Node.js + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: ${{ inputs.node-version }} + + - name: Save Node.js toolcache + if: ${{ runner.environment == 'self-hosted' && steps.node-cache.outputs.cache-hit != 'true' }} + uses: ./.github/actions/cache/save + with: + path: ${{ steps.node-toolcache.outputs.path }} + key: midscene-node-toolcache-v1-${{ runner.os }}-${{ runner.arch }}-${{ inputs.node-version }} + + - name: Setup pnpm + uses: pnpm/action-setup@eae0cfeb286e66ffb5155f1a79b90583a127a68b # v2.4.1 + with: + version: ${{ inputs.pnpm-version }} diff --git a/.github/actions/setup-puppeteer-browser/action.yml b/.github/actions/setup-puppeteer-browser/action.yml new file mode 100644 index 0000000000..0188be69a5 --- /dev/null +++ b/.github/actions/setup-puppeteer-browser/action.yml @@ -0,0 +1,71 @@ +name: Setup Puppeteer browser + +description: Restore and install the Puppeteer Chrome for Testing binary + +inputs: + package-directory: + description: Workspace package that provides the Puppeteer CLI + required: false + default: packages/web-integration + expose-as-chromium: + description: Add a chromium alias for tests that discover browsers from PATH + required: false + default: "false" + +outputs: + browser-path: + description: Absolute path to the Chrome for Testing executable + value: ${{ steps.browser.outputs.path }} + browser-cache-hit: + description: Whether the Puppeteer browser cache was restored + value: ${{ steps.browser-cache.outputs.cache-hit }} + +runs: + using: composite + steps: + - name: Restore Puppeteer browser + id: browser-cache + uses: ./.github/actions/cache/restore + with: + path: ~/.cache/puppeteer + key: midscene-puppeteer-v1-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('pnpm-lock.yaml') }} + restore-keys: | + midscene-puppeteer-v1-${{ runner.os }}-${{ runner.arch }}- + + - name: Install Chrome for Testing + shell: bash + run: pnpm --dir "${{ inputs.package-directory }}" exec puppeteer browsers install chrome + + - name: Save Puppeteer browser + if: ${{ steps.browser-cache.outputs.cache-hit != 'true' }} + uses: ./.github/actions/cache/save + with: + path: ~/.cache/puppeteer + key: midscene-puppeteer-v1-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('pnpm-lock.yaml') }} + + - name: Resolve Chrome for Testing + id: browser + shell: bash + run: | + set -euo pipefail + + browser_path="$( + pnpm --dir "${{ inputs.package-directory }}" exec node -e \ + 'process.stdout.write(require("puppeteer").executablePath())' + )" + if [ ! -x "$browser_path" ]; then + echo "::error::Puppeteer resolved a missing or non-executable browser: $browser_path" + exit 1 + fi + + echo "PUPPETEER_EXECUTABLE_PATH=$browser_path" >> "$GITHUB_ENV" + + if [ "${{ inputs.expose-as-chromium }}" = "true" ]; then + bin_dir="$RUNNER_TEMP/midscene-browser-bin" + mkdir -p "$bin_dir" + ln -sfn "$browser_path" "$bin_dir/chromium" + echo "$bin_dir" >> "$GITHUB_PATH" + fi + + echo "path=$browser_path" >> "$GITHUB_OUTPUT" + "$browser_path" --version diff --git a/.github/workflows/ai-unit-test.yml b/.github/workflows/ai-unit-test.yml index df7d834980..445a87e285 100644 --- a/.github/workflows/ai-unit-test.yml +++ b/.github/workflows/ai-unit-test.yml @@ -3,7 +3,17 @@ on: push: branches: - main + paths-ignore: + - '**/*.md' + - 'apps/site/**' + - '.changeset/**' + - 'docs/**' pull_request: + paths-ignore: + - '**/*.md' + - 'apps/site/**' + - '.changeset/**' + - 'docs/**' workflow_dispatch: inputs: branch: @@ -15,6 +25,10 @@ on: permissions: contents: read +concurrency: + group: ai-unit-test-${{ github.event.pull_request.number || inputs.branch || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + jobs: main: runs-on: ${{ fromJSON(vars.LINUX_SELF_HOSTED_RUNNER_LABELS || '"ubuntu-22.04"') }} @@ -36,30 +50,27 @@ jobs: steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: - fetch-depth: 0 ref: ${{ github.event.inputs.branch || github.ref }} - - name: Setup pnpm - uses: pnpm/action-setup@eae0cfeb286e66ffb5155f1a79b90583a127a68b # v2.4.1 - with: - version: 9.3.0 - - - name: Setup Node.js - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 - with: - node-version: '24.13.0' - cache: 'pnpm' + - name: Setup Node.js and pnpm + uses: ./.github/actions/setup-node-pnpm + + - name: Cache pnpm store + id: pnpm-cache + uses: ./.github/actions/pnpm-cache/restore - name: Install dependencies run: pnpm install --frozen-lockfile --ignore-scripts - - name: Install puppeteer dependencies - run: | - cd packages/web-integration - npx puppeteer browsers install chrome + - name: Save pnpm store + if: steps.pnpm-cache.outputs.cache-hit != 'true' + uses: ./.github/actions/pnpm-cache/save + + - name: Setup Puppeteer browser + uses: ./.github/actions/setup-puppeteer-browser - - name: Build project - run: pnpm run build + - name: Build AI unit test projects + run: pnpm exec nx run-many --target=build --projects="@midscene/report,@midscene/core,@midscene/web,@midscene/cli" - name: Run tests with coverage # Keep model request concurrency comparable to the former 4-core hosted runner. diff --git a/.github/workflows/ai.yml b/.github/workflows/ai.yml index a8b053dc5e..933d049f93 100644 --- a/.github/workflows/ai.yml +++ b/.github/workflows/ai.yml @@ -3,7 +3,17 @@ on: push: branches: - main + paths-ignore: + - '**/*.md' + - 'apps/site/**' + - '.changeset/**' + - 'docs/**' pull_request: + paths-ignore: + - '**/*.md' + - 'apps/site/**' + - '.changeset/**' + - 'docs/**' workflow_dispatch: inputs: branch: @@ -15,6 +25,10 @@ on: permissions: contents: read +concurrency: + group: ai-playwright-e2e-${{ github.event.pull_request.number || inputs.branch || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + env: MIDSCENE_MODEL_API_KEY: ${{ secrets.MIDSCENE_MODEL_API_KEY }} MIDSCENE_MODEL_BASE_URL: ${{ vars.MIDSCENE_MODEL_BASE_URL }} @@ -33,22 +47,17 @@ jobs: steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: - fetch-depth: 0 ref: ${{ github.event.inputs.branch || github.ref }} - - name: Setup pnpm - uses: pnpm/action-setup@eae0cfeb286e66ffb5155f1a79b90583a127a68b # v2.4.1 - with: - version: 9.3.0 + - name: Setup Node.js and pnpm + uses: ./.github/actions/setup-node-pnpm - - name: Setup Node.js - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 - with: - node-version: '24.13.0' - cache: 'pnpm' + - name: Cache pnpm store + id: pnpm-cache + uses: ./.github/actions/pnpm-cache/restore - name: Cache Playwright dependencies - uses: actions/cache@6f8efc29b200d32929f49075959781ed54ec270c # v3.5.0 + uses: ./.github/actions/cache/restore id: cache-playwright with: path: ~/.cache/ms-playwright @@ -59,22 +68,25 @@ jobs: - name: Install dependencies run: pnpm install --frozen-lockfile --ignore-scripts - - name: Install Playwright dependencies - run: | - cd packages/web-integration - if [ "${{ steps.cache-playwright.outputs.cache-hit }}" != "true" ]; then - npx playwright install --with-deps - else - npx playwright install-deps && npx playwright install - fi - - - name: Install puppeteer dependencies - run: | - cd packages/web-integration - npx puppeteer browsers install chrome + - name: Save pnpm store + if: steps.pnpm-cache.outputs.cache-hit != 'true' + uses: ./.github/actions/pnpm-cache/save + + - name: Setup headless browser + uses: ./.github/actions/setup-headless-browser - - name: Build project - run: pnpm run build + - name: Install Playwright Chromium + run: pnpm --dir packages/web-integration exec playwright install chromium + + - name: Save Playwright dependencies + if: steps.cache-playwright.outputs.cache-hit != 'true' + uses: ./.github/actions/cache/save + with: + path: ~/.cache/ms-playwright + key: ${{ runner.os }}-playwright-${{ hashFiles('pnpm-lock.yaml') }} + + - name: Build web e2e projects + run: pnpm exec nx run-many --target=build --projects="@midscene/report,@midscene/web,@midscene/cli" # Cache behavior uses explicit fixture configuration in the standard suite. # MIDSCENE_CACHE does not override those options, so a second full run is redundant. @@ -117,48 +129,24 @@ jobs: steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: - fetch-depth: 0 ref: ${{ github.event.inputs.branch || github.ref }} - - name: Setup pnpm - uses: pnpm/action-setup@eae0cfeb286e66ffb5155f1a79b90583a127a68b # v2.4.1 - with: - version: 9.3.0 + - name: Setup Node.js and pnpm + uses: ./.github/actions/setup-node-pnpm - - name: Setup Node.js - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 - with: - node-version: '24.13.0' - cache: 'pnpm' - - - name: Cache Playwright dependencies - uses: actions/cache@6f8efc29b200d32929f49075959781ed54ec270c # v3.5.0 - id: cache-playwright - with: - path: ~/.cache/ms-playwright - key: ${{ runner.os }}-playwright-${{ hashFiles('pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-playwright- + - name: Cache pnpm store + id: pnpm-cache + uses: ./.github/actions/pnpm-cache/restore - name: Install dependencies run: pnpm install --frozen-lockfile --ignore-scripts - - name: Install Playwright dependencies - run: | - cd packages/web-integration - if [ "${{ steps.cache-playwright.outputs.cache-hit }}" != "true" ]; then - npx playwright install --with-deps - else - npx playwright install-deps && npx playwright install - fi - - - name: Install puppeteer dependencies - run: | - cd packages/web-integration - npx puppeteer browsers install chrome + - name: Save pnpm store + if: steps.pnpm-cache.outputs.cache-hit != 'true' + uses: ./.github/actions/pnpm-cache/save - - name: Build project - run: pnpm run build + - name: Setup Puppeteer browser + uses: ./.github/actions/setup-puppeteer-browser - name: Run apps/report e2e tests run: cd apps/report && pnpm run e2e diff --git a/.github/workflows/android-emulator.yml b/.github/workflows/android-emulator.yml index 9699881bbd..76bf9d7024 100644 --- a/.github/workflows/android-emulator.yml +++ b/.github/workflows/android-emulator.yml @@ -30,6 +30,9 @@ concurrency: jobs: android-emulator: + concurrency: + group: ai-mobile-${{ github.event.pull_request.number || inputs.branch || github.ref }} + cancel-in-progress: false # The self-hosted Linux pool does not guarantee KVM passthrough. runs-on: ubuntu-22.04 timeout-minutes: 55 @@ -74,11 +77,18 @@ jobs: uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: '24.13.0' - cache: 'pnpm' + + - name: Cache pnpm store + id: pnpm-cache + uses: ./.github/actions/pnpm-cache/restore - name: Install dependencies run: pnpm install --frozen-lockfile --ignore-scripts + - name: Save pnpm store + if: steps.pnpm-cache.outputs.cache-hit != 'true' + uses: ./.github/actions/pnpm-cache/save + - name: Build report and Android packages id: build continue-on-error: true diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7402465bfb..12376f2406 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,7 +4,17 @@ on: push: branches: - main + paths-ignore: + - '**/*.md' + - 'apps/site/**' + - '.changeset/**' + - 'docs/**' pull_request: + paths-ignore: + - '**/*.md' + - 'apps/site/**' + - '.changeset/**' + - 'docs/**' workflow_dispatch: permissions: @@ -24,8 +34,6 @@ jobs: steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - with: - fetch-depth: 0 - name: Setup pnpm uses: pnpm/action-setup@eae0cfeb286e66ffb5155f1a79b90583a127a68b # v2.4.1 @@ -38,7 +46,7 @@ jobs: node-version: '24.13.0' - name: Cache Puppeteer dependencies - uses: actions/cache@6f8efc29b200d32929f49075959781ed54ec270c # v3.5.0 + uses: ./.github/actions/cache/restore id: cache-puppeteer with: path: ~/.cache/puppeteer @@ -54,6 +62,13 @@ jobs: cd packages/web-integration npx puppeteer browsers install chrome + - name: Save Puppeteer cache + if: steps.cache-puppeteer.outputs.cache-hit != 'true' + uses: ./.github/actions/cache/save + with: + path: ~/.cache/puppeteer + key: ${{ runner.os }}-puppeteer-${{ hashFiles('pnpm-lock.yaml') }} + - name: Check tsconfig references run: pnpm run check:references diff --git a/.github/workflows/headless-linux.yml b/.github/workflows/headless-linux.yml index 229149810b..7d377d7890 100644 --- a/.github/workflows/headless-linux.yml +++ b/.github/workflows/headless-linux.yml @@ -25,6 +25,10 @@ on: permissions: contents: read +concurrency: + group: headless-linux-${{ github.event.pull_request.number || inputs.branch || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + env: ELECTRON_SKIP_BINARY_DOWNLOAD: "1" @@ -47,43 +51,25 @@ jobs: with: ref: ${{ github.event.inputs.branch || github.ref }} - - name: Setup pnpm - uses: pnpm/action-setup@eae0cfeb286e66ffb5155f1a79b90583a127a68b # v2.4.1 - with: - version: 9.3.0 + - name: Setup Node.js and pnpm + uses: ./.github/actions/setup-node-pnpm - - name: Setup Node.js - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 - with: - node-version: '24.13.0' - cache: 'pnpm' - - - name: Install system dependencies - run: | - sudo apt-get update - sudo apt-get install -y \ - xvfb \ - x11-xserver-utils \ - imagemagick \ - libxtst6 \ - libxinerama1 \ - libx11-6 \ - libxkbcommon-x11-0 \ - libpng16-16 \ - libnss3 \ - libatk-bridge2.0-0 \ - libdrm2 \ - libgbm1 \ - libasound2 - # Install Google Chrome (not snap chromium which can't access Xvfb) - wget -q https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb - sudo dpkg -i google-chrome-stable_current_amd64.deb || sudo apt-get -f install -y + - name: Cache pnpm store + id: pnpm-cache + uses: ./.github/actions/pnpm-cache/restore - name: Install dependencies - run: pnpm install --frozen-lockfile + run: pnpm install --frozen-lockfile --ignore-scripts - - name: Build project - run: pnpm run build:skip-cache + - name: Save pnpm store + if: steps.pnpm-cache.outputs.cache-hit != 'true' + uses: ./.github/actions/pnpm-cache/save + + - name: Setup headless browser + uses: ./.github/actions/setup-headless-browser + + - name: Build AI todo projects + run: pnpm exec nx run-many --target=build --projects="@midscene/report,@midscene/computer" --skip-nx-cache - name: Verify system setup run: | @@ -94,7 +80,7 @@ jobs: echo "--- import (imagemagick) ---" which import && echo "import: OK" echo "--- Browser ---" - google-chrome-stable --version || echo "Chrome not found" + chromium --version || echo "Chrome not found" echo "--- DISPLAY ---" echo "DISPLAY=${DISPLAY:-(not set)}" @@ -137,43 +123,25 @@ jobs: with: ref: ${{ github.event.inputs.branch || github.ref }} - - name: Setup pnpm - uses: pnpm/action-setup@eae0cfeb286e66ffb5155f1a79b90583a127a68b # v2.4.1 - with: - version: 9.3.0 - - - name: Setup Node.js - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 - with: - node-version: '24.13.0' - cache: 'pnpm' + - name: Setup Node.js and pnpm + uses: ./.github/actions/setup-node-pnpm - - name: Install system dependencies - run: | - sudo apt-get update - sudo apt-get install -y \ - xvfb \ - x11-xserver-utils \ - imagemagick \ - libxtst6 \ - libxinerama1 \ - libx11-6 \ - libxkbcommon-x11-0 \ - libpng16-16 \ - libnss3 \ - libatk-bridge2.0-0 \ - libdrm2 \ - libgbm1 \ - libasound2 - # Install Google Chrome (not snap chromium which can't access Xvfb) - wget -q https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb - sudo dpkg -i google-chrome-stable_current_amd64.deb || sudo apt-get -f install -y + - name: Cache pnpm store + id: pnpm-cache + uses: ./.github/actions/pnpm-cache/restore - name: Install dependencies - run: pnpm install --frozen-lockfile + run: pnpm install --frozen-lockfile --ignore-scripts + + - name: Save pnpm store + if: steps.pnpm-cache.outputs.cache-hit != 'true' + uses: ./.github/actions/pnpm-cache/save + + - name: Setup headless browser + uses: ./.github/actions/setup-headless-browser - - name: Build project - run: pnpm run build:skip-cache + - name: Build Chrome extension projects + run: pnpm exec nx run-many --target=build --projects="@midscene/report,chrome-extension" --skip-nx-cache - name: Run Chrome extension test run: AI_TEST_TYPE=computer npx nx test @midscene/computer -- tests/ai/chrome-extension.test.ts @@ -212,46 +180,29 @@ jobs: with: ref: ${{ github.event.inputs.branch || github.ref }} - - name: Setup pnpm - uses: pnpm/action-setup@eae0cfeb286e66ffb5155f1a79b90583a127a68b # v2.4.1 - with: - version: 9.3.0 - - - name: Setup Node.js - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 - with: - node-version: '24.13.0' - cache: 'pnpm' + - name: Setup Node.js and pnpm + uses: ./.github/actions/setup-node-pnpm - - name: Install system dependencies - run: | - sudo apt-get update - sudo apt-get install -y \ - xvfb \ - x11-xserver-utils \ - imagemagick \ - libxtst6 \ - libxinerama1 \ - libx11-6 \ - libxkbcommon-x11-0 \ - libpng16-16 \ - libnss3 \ - libatk-bridge2.0-0 \ - libdrm2 \ - libgbm1 \ - libasound2 - wget -q https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb - sudo dpkg -i google-chrome-stable_current_amd64.deb || sudo apt-get -f install -y + - name: Cache pnpm store + id: pnpm-cache + uses: ./.github/actions/pnpm-cache/restore - name: Install dependencies - run: pnpm install --frozen-lockfile + run: pnpm install --frozen-lockfile --ignore-scripts + + - name: Save pnpm store + if: steps.pnpm-cache.outputs.cache-hit != 'true' + uses: ./.github/actions/pnpm-cache/save - - name: Build project - run: pnpm run build:skip-cache + - name: Setup headless browser + uses: ./.github/actions/setup-headless-browser + + - name: Build Chrome extension projects + run: pnpm exec nx run-many --target=build --projects="@midscene/report,chrome-extension" --skip-nx-cache - name: Run Bridge mode start/stop test run: AI_TEST_TYPE=computer npx nx test @midscene/computer -- tests/ai/chrome-extension-bridge.test.ts - timeout-minutes: 25 + timeout-minutes: 40 id: test-bridge continue-on-error: true @@ -286,42 +237,25 @@ jobs: with: ref: ${{ github.event.inputs.branch || github.ref }} - - name: Setup pnpm - uses: pnpm/action-setup@eae0cfeb286e66ffb5155f1a79b90583a127a68b # v2.4.1 - with: - version: 9.3.0 + - name: Setup Node.js and pnpm + uses: ./.github/actions/setup-node-pnpm - - name: Setup Node.js - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 - with: - node-version: '24.13.0' - cache: 'pnpm' - - - name: Install system dependencies - run: | - sudo apt-get update - sudo apt-get install -y \ - xvfb \ - x11-xserver-utils \ - imagemagick \ - libxtst6 \ - libxinerama1 \ - libx11-6 \ - libxkbcommon-x11-0 \ - libpng16-16 \ - libnss3 \ - libatk-bridge2.0-0 \ - libdrm2 \ - libgbm1 \ - libasound2 - wget -q https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb - sudo dpkg -i google-chrome-stable_current_amd64.deb || sudo apt-get -f install -y + - name: Cache pnpm store + id: pnpm-cache + uses: ./.github/actions/pnpm-cache/restore - name: Install dependencies - run: pnpm install --frozen-lockfile + run: pnpm install --frozen-lockfile --ignore-scripts - - name: Build project - run: pnpm run build:skip-cache + - name: Save pnpm store + if: steps.pnpm-cache.outputs.cache-hit != 'true' + uses: ./.github/actions/pnpm-cache/save + + - name: Setup headless browser + uses: ./.github/actions/setup-headless-browser + + - name: Build Chrome extension projects + run: pnpm exec nx run-many --target=build --projects="@midscene/report,chrome-extension" --skip-nx-cache - name: Run Playground advanced tests run: AI_TEST_TYPE=computer npx nx test @midscene/computer -- tests/ai/chrome-extension-playground.test.ts @@ -360,42 +294,25 @@ jobs: with: ref: ${{ github.event.inputs.branch || github.ref }} - - name: Setup pnpm - uses: pnpm/action-setup@eae0cfeb286e66ffb5155f1a79b90583a127a68b # v2.4.1 - with: - version: 9.3.0 + - name: Setup Node.js and pnpm + uses: ./.github/actions/setup-node-pnpm - - name: Setup Node.js - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 - with: - node-version: '24.13.0' - cache: 'pnpm' - - - name: Install system dependencies - run: | - sudo apt-get update - sudo apt-get install -y \ - xvfb \ - x11-xserver-utils \ - imagemagick \ - libxtst6 \ - libxinerama1 \ - libx11-6 \ - libxkbcommon-x11-0 \ - libpng16-16 \ - libnss3 \ - libatk-bridge2.0-0 \ - libdrm2 \ - libgbm1 \ - libasound2 - wget -q https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb - sudo dpkg -i google-chrome-stable_current_amd64.deb || sudo apt-get -f install -y + - name: Cache pnpm store + id: pnpm-cache + uses: ./.github/actions/pnpm-cache/restore - name: Install dependencies - run: pnpm install --frozen-lockfile + run: pnpm install --frozen-lockfile --ignore-scripts + + - name: Save pnpm store + if: steps.pnpm-cache.outputs.cache-hit != 'true' + uses: ./.github/actions/pnpm-cache/save + + - name: Setup headless browser + uses: ./.github/actions/setup-headless-browser - - name: Build project - run: pnpm run build:skip-cache + - name: Build Chrome extension projects + run: pnpm exec nx run-many --target=build --projects="@midscene/report,chrome-extension" --skip-nx-cache - name: Run Recorder mode tests run: AI_TEST_TYPE=computer npx nx test @midscene/computer -- tests/ai/chrome-extension-recorder.test.ts @@ -434,42 +351,25 @@ jobs: with: ref: ${{ github.event.inputs.branch || github.ref }} - - name: Setup pnpm - uses: pnpm/action-setup@eae0cfeb286e66ffb5155f1a79b90583a127a68b # v2.4.1 - with: - version: 9.3.0 + - name: Setup Node.js and pnpm + uses: ./.github/actions/setup-node-pnpm - - name: Setup Node.js - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 - with: - node-version: '24.13.0' - cache: 'pnpm' - - - name: Install system dependencies - run: | - sudo apt-get update - sudo apt-get install -y \ - xvfb \ - x11-xserver-utils \ - imagemagick \ - libxtst6 \ - libxinerama1 \ - libx11-6 \ - libxkbcommon-x11-0 \ - libpng16-16 \ - libnss3 \ - libatk-bridge2.0-0 \ - libdrm2 \ - libgbm1 \ - libasound2 - wget -q https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb - sudo dpkg -i google-chrome-stable_current_amd64.deb || sudo apt-get -f install -y + - name: Cache pnpm store + id: pnpm-cache + uses: ./.github/actions/pnpm-cache/restore - name: Install dependencies - run: pnpm install --frozen-lockfile + run: pnpm install --frozen-lockfile --ignore-scripts + + - name: Save pnpm store + if: steps.pnpm-cache.outputs.cache-hit != 'true' + uses: ./.github/actions/pnpm-cache/save + + - name: Setup headless browser + uses: ./.github/actions/setup-headless-browser - - name: Build project - run: pnpm run build:skip-cache + - name: Build Chrome extension projects + run: pnpm exec nx run-many --target=build --projects="@midscene/report,chrome-extension" --skip-nx-cache - name: Run Settings and cross-mode tests run: AI_TEST_TYPE=computer npx nx test @midscene/computer -- tests/ai/chrome-extension-settings.test.ts diff --git a/.github/workflows/ios-simulator.yml b/.github/workflows/ios-simulator.yml index 971b0a74eb..d507a6ef67 100644 --- a/.github/workflows/ios-simulator.yml +++ b/.github/workflows/ios-simulator.yml @@ -31,6 +31,9 @@ concurrency: jobs: ios-simulator: + concurrency: + group: ai-mobile-${{ github.event.pull_request.number || inputs.branch || github.ref }} + cancel-in-progress: false runs-on: ${{ fromJSON(vars.MAC_SELF_HOSTED_RUNNER_LABELS || '"macos-latest"') }} timeout-minutes: 70 env: @@ -73,20 +76,20 @@ jobs: xcrun simctl list devices available } 2>&1 | tee "$MIDSCENE_IOS_DIAGNOSTICS_DIR/runner-preflight.log" - - name: Setup pnpm - uses: pnpm/action-setup@eae0cfeb286e66ffb5155f1a79b90583a127a68b # v2.4.1 - with: - version: 9.3.0 + - name: Setup Node.js and pnpm + uses: ./.github/actions/setup-node-pnpm - - name: Setup Node.js - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 - with: - node-version: '24.13.0' - cache: 'pnpm' + - name: Cache pnpm store + id: pnpm-cache + uses: ./.github/actions/pnpm-cache/restore - name: Install dependencies run: pnpm install --frozen-lockfile --ignore-scripts + - name: Save pnpm store + if: steps.pnpm-cache.outputs.cache-hit != 'true' + uses: ./.github/actions/pnpm-cache/save + - name: Build report and iOS packages id: build continue-on-error: true diff --git a/.github/workflows/issue-labeled.yml b/.github/workflows/issue-labeled.yml index 5393745442..45b7d0feba 100644 --- a/.github/workflows/issue-labeled.yml +++ b/.github/workflows/issue-labeled.yml @@ -8,12 +8,11 @@ jobs: reply-labeled: name: Reply need reproduction runs-on: ${{ fromJSON(vars.CI_LINUX_MINI_RUNNER || '"ubuntu-latest"') }} - if: github.repository == 'web-infra-dev/midscene' + if: github.repository == 'web-infra-dev/midscene' && github.event.label.name == 'need reproduction' permissions: issues: write steps: - name: need reproduction - if: github.event.label.name == 'need reproduction' env: GH_TOKEN: ${{ github.token }} ISSUE_NUMBER: ${{ github.event.issue.number }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index e86a544fca..91ddbf4105 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -20,26 +20,21 @@ jobs: steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - with: - fetch-depth: 0 - - - name: Fetch all branches - run: git fetch --all - - - name: Setup pnpm - uses: pnpm/action-setup@eae0cfeb286e66ffb5155f1a79b90583a127a68b # v2.4.1 - with: - version: 9.3.0 - - - name: Setup Node.js - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 - with: - node-version: '24.13.0' - cache: 'pnpm' + + - name: Setup Node.js and pnpm + uses: ./.github/actions/setup-node-pnpm + + - name: Cache pnpm store + id: pnpm-cache + uses: ./.github/actions/pnpm-cache/restore - name: Install Dependencies run: pnpm install --frozen-lockfile --ignore-scripts + - name: Save pnpm store + if: steps.pnpm-cache.outputs.cache-hit != 'true' + uses: ./.github/actions/pnpm-cache/save + - name: Check Dependency Version run: pnpm run check-dependency-version diff --git a/.github/workflows/macos-desktop.yml b/.github/workflows/macos-desktop.yml index 0f777f20ad..f9935623f7 100644 --- a/.github/workflows/macos-desktop.yml +++ b/.github/workflows/macos-desktop.yml @@ -30,6 +30,9 @@ concurrency: jobs: macos-desktop: + concurrency: + group: ai-desktop-${{ github.event.pull_request.number || inputs.branch || github.ref }} + cancel-in-progress: false # The self-hosted macOS runner is ARM and has no interactive GUI session. runs-on: macos-15-intel timeout-minutes: 50 @@ -83,11 +86,18 @@ jobs: uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: '24.13.0' - cache: 'pnpm' + + - name: Cache pnpm store + id: pnpm-cache + uses: ./.github/actions/pnpm-cache/restore - name: Install dependencies run: pnpm install --frozen-lockfile --ignore-scripts + - name: Save pnpm store + if: steps.pnpm-cache.outputs.cache-hit != 'true' + uses: ./.github/actions/pnpm-cache/save + - name: Build report and computer packages id: build continue-on-error: true diff --git a/.github/workflows/pr-title.yml b/.github/workflows/pr-title.yml index 2efd83998b..f002a48c92 100644 --- a/.github/workflows/pr-title.yml +++ b/.github/workflows/pr-title.yml @@ -2,7 +2,7 @@ name: PR Title on: pull_request: - types: [opened, edited, synchronize, reopened] + types: [opened, edited, reopened] permissions: contents: read @@ -10,25 +10,26 @@ permissions: jobs: commitlint: name: Validate title + if: github.event.action != 'edited' || github.event.changes.title runs-on: ${{ fromJSON(vars.CI_LINUX_MINI_RUNNER || '"ubuntu-latest"') }} steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - - name: Setup pnpm - uses: pnpm/action-setup@eae0cfeb286e66ffb5155f1a79b90583a127a68b # v2.4.1 - with: - version: 9.3.0 + - name: Setup Node.js and pnpm + uses: ./.github/actions/setup-node-pnpm - - name: Setup Node.js - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 - with: - node-version: '24.13.0' - cache: 'pnpm' + - name: Cache pnpm store + id: pnpm-cache + uses: ./.github/actions/pnpm-cache/restore - name: Install dependencies run: pnpm install --frozen-lockfile --ignore-scripts + - name: Save pnpm store + if: steps.pnpm-cache.outputs.cache-hit != 'true' + uses: ./.github/actions/pnpm-cache/save + - name: Validate PR title env: PR_TITLE: ${{ github.event.pull_request.title }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5c5983af99..d3b37649c3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -24,7 +24,8 @@ jobs: outputs: version: ${{ steps.get_version.outputs.version }} is_prerelease: ${{ steps.get_version.outputs.is_prerelease }} - runs-on: ${{ fromJSON(vars.LINUX_SELF_HOSTED_RUNNER_LABELS || '"ubuntu-22.04"') }} + # npm Trusted Publishing currently requires a GitHub-hosted runner. + runs-on: ubuntu-22.04 strategy: matrix: node-version: [24.13.0] @@ -62,11 +63,15 @@ jobs: uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: '24.13.0' - cache: 'pnpm' registry-url: 'https://registry.npmjs.org' + - name: Cache pnpm store + id: pnpm-cache + uses: ./.github/actions/pnpm-cache/restore + - name: Cache Puppeteer - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + id: cache-puppeteer + uses: ./.github/actions/cache/restore with: path: ~/.cache/puppeteer key: ${{ runner.os }}-puppeteer-${{ hashFiles('**/package-lock.json') }} @@ -76,9 +81,20 @@ jobs: - name: Install dependencies run: pnpm install --frozen-lockfile --ignore-scripts + - name: Save pnpm store + if: steps.pnpm-cache.outputs.cache-hit != 'true' + uses: ./.github/actions/pnpm-cache/save + - name: Install Puppeteer browser run: cd packages/web-integration && npx puppeteer browsers install chrome + - name: Save Puppeteer cache + if: steps.cache-puppeteer.outputs.cache-hit != 'true' + uses: ./.github/actions/cache/save + with: + path: ~/.cache/puppeteer + key: ${{ runner.os }}-puppeteer-${{ hashFiles('**/package-lock.json') }} + - name: Install @midscene/computer native build deps (linux/x64) # The prepublishOnly hook of @midscene/computer compiles # bin/linux/rdp-helper on the publishing runner so the linux helper @@ -233,7 +249,10 @@ jobs: uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: '22.11.0' - cache: 'pnpm' + + - name: Cache pnpm store + id: pnpm-cache + uses: ./.github/actions/pnpm-cache/restore - name: Prepare macOS codesigning keychain if: ${{ matrix.platform == 'darwin' }} @@ -290,6 +309,10 @@ jobs: - name: Install dependencies run: pnpm install --frozen-lockfile --ignore-scripts + - name: Save pnpm store + if: steps.pnpm-cache.outputs.cache-hit != 'true' + uses: ./.github/actions/pnpm-cache/save + - name: Install @midscene/computer native build deps (macOS) if: ${{ matrix.platform == 'darwin' }} run: brew install cmake pkg-config freerdp diff --git a/.github/workflows/studio-headless-linux.yml b/.github/workflows/studio-headless-linux.yml index 3bea7f6194..c1e08dd1c4 100644 --- a/.github/workflows/studio-headless-linux.yml +++ b/.github/workflows/studio-headless-linux.yml @@ -44,7 +44,19 @@ jobs: uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: '24.13.0' - cache: 'pnpm' + + - name: Cache pnpm store + id: pnpm-cache + uses: ./.github/actions/pnpm-cache/restore + + - name: Cache Electron binary + id: electron-cache + uses: ./.github/actions/cache/restore + with: + path: ~/.cache/electron + key: midscene-electron-v1-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('pnpm-lock.yaml') }} + restore-keys: | + midscene-electron-v1-${{ runner.os }}-${{ runner.arch }}- - name: Install system dependencies run: | @@ -76,18 +88,24 @@ jobs: - name: Install dependencies run: pnpm install --frozen-lockfile + env: + PUPPETEER_SKIP_DOWNLOAD: 'true' - - name: Install Puppeteer browser binary - # `pnpm install` normally triggers puppeteer's `postinstall` to fetch - # the Chrome binary into `~/.cache/puppeteer`, but the added - # `optionalDependencies` (appdmg) shifts the install layout enough - # that the postinstall sometimes does not fire on the headless Linux - # runner. The Studio Web playground session needs that Chrome to - # launch its puppeteer page; without it `createSession` throws - # "Could not find Chrome" and the web preview e2e asserts on a - # permanently disconnected device. Install it explicitly so the test - # environment is deterministic. - run: pnpm --dir apps/studio exec puppeteer browsers install chrome + - name: Save pnpm store + if: steps.pnpm-cache.outputs.cache-hit != 'true' + uses: ./.github/actions/pnpm-cache/save + + - name: Save Electron binary + if: steps.electron-cache.outputs.cache-hit != 'true' + uses: ./.github/actions/cache/save + with: + path: ~/.cache/electron + key: midscene-electron-v1-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('pnpm-lock.yaml') }} + + - name: Setup Puppeteer browser + uses: ./.github/actions/setup-puppeteer-browser + with: + package-directory: apps/studio - name: Build Studio run: npx nx run-many --target=build --projects=studio,@midscene/report diff --git a/.github/workflows/studio-package-validation.yml b/.github/workflows/studio-package-validation.yml index a457c25cab..987524a4e6 100644 --- a/.github/workflows/studio-package-validation.yml +++ b/.github/workflows/studio-package-validation.yml @@ -52,7 +52,10 @@ jobs: uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: '22.11.0' - cache: 'pnpm' + + - name: Cache pnpm store + id: pnpm-cache + uses: ./.github/actions/pnpm-cache/restore - name: Prepare macOS codesigning keychain if: ${{ matrix.platform == 'darwin' }} @@ -109,6 +112,10 @@ jobs: - name: Install dependencies run: pnpm install --frozen-lockfile --ignore-scripts + - name: Save pnpm store + if: steps.pnpm-cache.outputs.cache-hit != 'true' + uses: ./.github/actions/pnpm-cache/save + - name: Install @midscene/computer native build deps (macOS) if: ${{ matrix.platform == 'darwin' }} run: brew install cmake pkg-config freerdp diff --git a/.github/workflows/windows-desktop.yml b/.github/workflows/windows-desktop.yml index c381501b42..11aedcba15 100644 --- a/.github/workflows/windows-desktop.yml +++ b/.github/workflows/windows-desktop.yml @@ -30,6 +30,9 @@ concurrency: jobs: windows-desktop: + concurrency: + group: ai-desktop-${{ github.event.pull_request.number || inputs.branch || github.ref }} + cancel-in-progress: false # The self-hosted Windows runner runs as a non-interactive service. runs-on: windows-2022 timeout-minutes: 50 @@ -169,12 +172,19 @@ jobs: uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: '24.13.0' - cache: 'pnpm' + + - name: Cache pnpm store + id: pnpm-cache + uses: ./.github/actions/pnpm-cache/restore - name: Install dependencies shell: pwsh run: pnpm install --frozen-lockfile --ignore-scripts + - name: Save pnpm store + if: steps.pnpm-cache.outputs.cache-hit != 'true' + uses: ./.github/actions/pnpm-cache/save + - name: Build report and computer packages id: build continue-on-error: true diff --git a/apps/report/e2e/detail-panel.yaml b/apps/report/e2e/detail-panel.yaml index 07340e72b0..46630bcd15 100644 --- a/apps/report/e2e/detail-panel.yaml +++ b/apps/report/e2e/detail-panel.yaml @@ -5,21 +5,21 @@ tasks: - name: view screenshots tab flow: - sleep: 2000 - - ai: Click on the first task row in the left sidebar that has type "Planning" or "Insight" or "Locate" + - aiTap: the first task row in the left sidebar that has type "Planning" or "Insight" or "Locate" - sleep: 2000 - - ai: In the center task detail Replay/Screenshots/JSON View switcher, select "Screenshots" (not Replay or JSON View) and wait until screenshot thumbnails are displayed + - aiTap: the "Screenshots" option in the center task detail Replay/Screenshots/JSON View switcher - sleep: 1500 - aiAssert: In the center task detail panel, the selected tab among Replay, Screenshots, and JSON View is Screenshots. The content below the tabs shows UI screenshot images for the selected task. - name: view JSON data tab flow: - - ai: In the center task detail area, click the "JSON View" option in the Replay/Screenshots/JSON View switcher and wait until the content changes to the text data view + - aiTap: the "JSON View" option in the center task detail Replay/Screenshots/JSON View switcher - sleep: 1000 - aiAssert: In the center task detail panel, the selected tab among Replay, Screenshots, and JSON View is JSON View. The content below the tabs shows structured task details as JSON text instead of UI screenshot images. - name: inspect detail side sections flow: - - ai: Click on the first Planning task row in the left sidebar + - aiTap: the first Planning task row in the left sidebar - sleep: 1000 - aiAssert: The right detail side panel shows collapsible section headers named Param and Output, and the Param section is expanded with task input data # Collapse both Param and the Output/Element section so the Meta section @@ -35,6 +35,6 @@ tasks: - name: open in playground button flow: - - ai: Click on a task row in the sidebar that shows "Planning" type + - aiTap: a task row in the sidebar that shows "Planning" type - sleep: 2000 - aiAssert: There is a "Open in Playground" button visible on the page diff --git a/apps/report/e2e/report-single.yaml b/apps/report/e2e/report-single.yaml index 8ebee7af3d..f026e14c66 100644 --- a/apps/report/e2e/report-single.yaml +++ b/apps/report/e2e/report-single.yaml @@ -27,16 +27,3 @@ tasks: deepLocate: true - sleep: 1000 - aiAssert: The player settings popup is open above the bottom-right control bar, showing options such as "Focus on cursor", "Subtitle", "Playback speed", and speed choices like "0.5x", "1x", "1.5x", or "2x". - - # --- Dark mode --- - - name: toggle dark mode - flow: - - aiTap: theme toggle button in the top right header - - sleep: 2000 - - aiAssert: The report UI is in dark theme; the top header, left sidebar, and page chrome are dark charcoal or black rather than white - - - name: toggle back to light mode - flow: - - aiTap: theme toggle button in the top right header - - sleep: 2000 - - aiAssert: The report UI is in light theme; the top header, left sidebar, and page chrome are light or white rather than dark charcoal or black diff --git a/apps/report/e2e/timeline-interaction.yaml b/apps/report/e2e/timeline-interaction.yaml index 3880ec13e4..f41b12d408 100644 --- a/apps/report/e2e/timeline-interaction.yaml +++ b/apps/report/e2e/timeline-interaction.yaml @@ -9,6 +9,6 @@ tasks: - name: click timeline screenshot to navigate flow: - - ai: Click on a screenshot thumbnail in the timeline area + - aiTap: a screenshot thumbnail in the timeline area - sleep: 2000 - aiAssert: The sidebar highlights a task row and the detail panel shows information for the selected task diff --git a/apps/report/package.json b/apps/report/package.json index 703be9717c..7fa73af88f 100644 --- a/apps/report/package.json +++ b/apps/report/package.json @@ -11,7 +11,7 @@ "test": "vitest --run", "test:watch": "vitest", "generate-demo": "node scripts/generate-demo-report.mjs", - "e2e": "pnpm --dir ../.. exec nx build @midscene/report --verbose && node scripts/inject-report-template.mjs && node scripts/generate-demo-report.mjs && node ../../packages/cli/bin/midscene ./e2e/" + "e2e": "pnpm --dir ../.. exec nx run-many --target=build --projects=\"@midscene/report,@midscene/cli\" --verbose && node scripts/inject-report-template.mjs && node scripts/generate-demo-report.mjs && node ../../packages/cli/bin/midscene ./e2e/ --concurrent 2" }, "dependencies": { "@ant-design/icons": "^5.3.1", diff --git a/packages/computer/tests/ai/chrome-extension-bridge.test.ts b/packages/computer/tests/ai/chrome-extension-bridge.test.ts index 9c24979c19..b539560060 100644 --- a/packages/computer/tests/ai/chrome-extension-bridge.test.ts +++ b/packages/computer/tests/ai/chrome-extension-bridge.test.ts @@ -207,7 +207,7 @@ describe('chrome extension bridge mode start/stop (#2119)', () => { ); } }, - { timeout: 12 * 60 * 1000, retry: 0 }, + { timeout: 20 * 60 * 1000, retry: 0 }, ); // ── Test: bridge connects to a real server ──────────────────────────── diff --git a/packages/computer/tests/ai/chrome-extension-helpers.ts b/packages/computer/tests/ai/chrome-extension-helpers.ts index bdb7553a06..d52d29258f 100644 --- a/packages/computer/tests/ai/chrome-extension-helpers.ts +++ b/packages/computer/tests/ai/chrome-extension-helpers.ts @@ -45,6 +45,16 @@ const EXTENSION_ENV_KEYS = [ // ─── Browser Helpers ──────────────────────────────────────────────────────── function findExtensionCapableBrowser(): string { + const configuredBrowser = process.env.PUPPETEER_EXECUTABLE_PATH; + if (configuredBrowser) { + if (!fs.existsSync(configuredBrowser)) { + throw new Error( + `PUPPETEER_EXECUTABLE_PATH does not exist: ${configuredBrowser}`, + ); + } + return configuredBrowser; + } + // Check puppeteer cache first (Chrome for Testing supports --load-extension) const puppeteerBase = path.join( process.env.HOME ?? '~', diff --git a/packages/computer/tests/ai/chrome-extension-playground.test.ts b/packages/computer/tests/ai/chrome-extension-playground.test.ts index 03f5e5b90c..608bea7ff8 100644 --- a/packages/computer/tests/ai/chrome-extension-playground.test.ts +++ b/packages/computer/tests/ai/chrome-extension-playground.test.ts @@ -74,6 +74,23 @@ describe('chrome extension playground advanced tests', () => { } }); + it('action type switching changes the composer placeholder', async () => { + await agent.aiAct(`Click the "Query" button in ${SIDE_PANEL}`); + await sleep(500); + await agent.aiAssert( + `${SIDE_PANEL} shows an input area with placeholder text containing "query"`, + ); + + await agent.aiAct(`Click the "Assert" button in ${SIDE_PANEL}`); + await sleep(500); + await agent.aiAssert( + `${SIDE_PANEL} shows an input area with placeholder text containing "assert"`, + ); + + await agent.aiAct(`Click the "Action" button in ${SIDE_PANEL}`); + await sleep(500); + }); + it('aiQuery: extract page title and verify result', async () => { await agent.aiAct(`Click the "aiQuery" button in ${SIDE_PANEL}`); await sleep(500); diff --git a/packages/computer/tests/ai/chrome-extension.test.ts b/packages/computer/tests/ai/chrome-extension.test.ts index 7f0e82b4f6..b6bb25c4f1 100644 --- a/packages/computer/tests/ai/chrome-extension.test.ts +++ b/packages/computer/tests/ai/chrome-extension.test.ts @@ -1,3 +1,7 @@ +/** + * Basic extension launch smoke test. Playground, Bridge, Recorder, and + * Settings behavior lives in their dedicated parallel suites. + */ import path from 'node:path'; import { sleep } from '@midscene/core/utils'; import { beforeAll, describe, it, vi } from 'vitest'; @@ -12,9 +16,6 @@ import { vi.setConfig({ testTimeout: 360 * 1000 }); -const SIDE_PANEL = - 'the Midscene side panel on the right side of the browser window'; - describe('chrome extension smoke test', () => { let agent: ComputerAgent; let extId: string; @@ -36,8 +37,6 @@ describe('chrome extension smoke test', () => { console.log('Extension ID:', extId); }); - // ── 1. Side Panel Launch ────────────────────────────────────────────── - it('open side panel via extension icon', async () => { await agent.aiAct( 'Click the puzzle piece icon (Extensions button) in the top-right area of the Chrome toolbar', @@ -59,131 +58,4 @@ describe('chrome extension smoke test', () => { await sleep(3000); } }); - - // ── 2. Playground UI Elements ─────────────────────────────────────── - - it('playground: UI elements are rendered correctly', async () => { - await agent.aiAssert( - `${SIDE_PANEL} shows: (1) action type buttons like "Tap" and "Query", (2) a text input area with a "Run" button, (3) a gear/settings icon`, - ); - }); - - // ── 3. Action Type Switching ────────────────────────────────────────── - - it('playground: action type switching changes placeholder', async () => { - await agent.aiAct(`Click the "Query" button in ${SIDE_PANEL}`); - await sleep(500); - await agent.aiAssert( - `${SIDE_PANEL} shows an input area with placeholder text containing "query"`, - ); - - await agent.aiAct(`Click the "Assert" button in ${SIDE_PANEL}`); - await sleep(500); - await agent.aiAssert( - `${SIDE_PANEL} shows an input area with placeholder text containing "assert"`, - ); - - // Switch back to Tap mode for the next test. - await agent.aiAct(`Click the "Tap" button in ${SIDE_PANEL}`); - await sleep(500); - }); - - // ── 4. Run aiAct Task ───────────────────────────────────────────────── - - it('playground: run aiAct to add a todo item', async () => { - await agent.aiAct(`Click the "Action" button in ${SIDE_PANEL}`); - await sleep(500); - - await agent.aiAct( - `In ${SIDE_PANEL}, click the text input area and type: Enter "Learn JS today" in the task box, then press Enter`, - ); - await sleep(500); - - await agent.aiAct(`Click the "Run" button in ${SIDE_PANEL}`); - await sleep(20000); - - await agent.aiAssert( - 'The TodoMVC page on the left shows a todo item containing "Learn JS today" OR the side panel shows execution progress/result', - ); - }); - - // ── 5. Mode Switching ────────────────────────────────────────────────── - - it( - 'mode switching: switch to Bridge and back', - { timeout: 12 * 60 * 1000, retry: 0 }, - async () => { - // Switch to Bridge Mode with retry - the dropdown menu can be flaky in headless - const switchToBridge = async () => { - await agent.aiAct( - `In ${SIDE_PANEL}, find and click the hamburger menu icon (three horizontal lines "≡") at the top-left corner. It should open a dropdown menu.`, - ); - await sleep(2000); - await agent.aiAct( - 'In the dropdown menu that just appeared, click the menu item labeled "Bridge Mode" which has an API icon next to it', - ); - await sleep(3000); - }; - - // First attempt - await switchToBridge(); - - // Verify or retry once if needed - try { - await agent.aiAssert( - `${SIDE_PANEL} shows Bridge mode UI with text containing "Listening" or "Disconnected" or "Bridge" connection status`, - ); - } catch { - // Retry: the dropdown click may not have registered - console.log('Bridge mode switch failed, retrying...'); - await switchToBridge(); - await agent.aiAssert( - `${SIDE_PANEL} shows Bridge mode UI with text containing "Listening" or "Disconnected" or "Bridge" connection status`, - ); - } - - // Switch back to Playground - await agent.aiAct( - `In ${SIDE_PANEL}, find and click the hamburger menu icon (three horizontal lines "≡") at the top-left corner`, - ); - await sleep(2000); - await agent.aiAct( - 'In the dropdown menu that just appeared, click the menu item labeled "Playground"', - ); - await sleep(3000); - }, - ); - - // ── 6. Settings Modal ───────────────────────────────────────────────── - - it('settings: open and close env config modal', async () => { - const openEnvConfigModal = async () => { - await agent.aiAct( - `Click the settings icon in the top-right header area of ${SIDE_PANEL}, next to the GitHub and help icons. Do not click the run configuration icon inside the prompt composer.`, - ); - await sleep(1500); - }; - - await openEnvConfigModal(); - - try { - await agent.aiAssert( - 'A modal titled "Model Env Config" is visible, and it contains a large text area for environment variable configuration.', - ); - } catch { - await openEnvConfigModal(); - await agent.aiAssert( - 'A modal titled "Model Env Config" is visible, and it contains a large text area for environment variable configuration.', - ); - } - - await agent.aiAct( - 'Click the X close button in the top-right corner of the "Model Env Config" modal.', - ); - await sleep(1500); - - await agent.aiAssert( - `The "Model Env Config" modal is closed, and ${SIDE_PANEL} is visible showing Playground UI without any modal overlay.`, - ); - }); }); diff --git a/packages/web-integration/tests/ai/web/playwright-reporter-test/todo-report.spec.ts b/packages/web-integration/tests/ai/web/playwright-reporter-test/todo-report.spec.ts index 41e11104d9..ee42a1b7e1 100644 --- a/packages/web-integration/tests/ai/web/playwright-reporter-test/todo-report.spec.ts +++ b/packages/web-integration/tests/ai/web/playwright-reporter-test/todo-report.spec.ts @@ -18,9 +18,14 @@ test('ai report', async ({ page, aiAssert, aiQuery }, testInfo) => { const options = page.locator('.selector-content .option-item'); await expect(options.first()).toBeVisible(); - const aiTodoOption = options.filter({ hasText: /ai todo/i }); - if ((await aiTodoOption.count()) > 0) { - await aiTodoOption.first().click(); + const aiTodoOptions = options.filter({ hasText: /ai todo/i }); + const successfulAiTodoOptions = aiTodoOptions.filter({ + has: page.locator('[aria-label="check"]'), + }); + if ((await successfulAiTodoOptions.count()) > 0) { + await successfulAiTodoOptions.last().click(); + } else if ((await aiTodoOptions.count()) > 0) { + await aiTodoOptions.last().click(); } else { await options.first().click(); }