From 8803649179ec7daf74d22064ec2e7b60d835f358 Mon Sep 17 00:00:00 2001 From: ylemkimon Date: Sat, 19 Dec 2020 09:16:34 +0900 Subject: [PATCH] ci: run screenshotter in container (#2644) * ci: run screenshotter in container Co-authored-by: Kevin Barabash --- .github/workflows/ci.yml | 131 +-------------- .github/workflows/screenshotter.yml | 98 ++++++++++++ dockers/screenshotter/screenshotter.js | 29 +++- package.json | 1 + renovate.json | 4 +- yarn.lock | 213 ++++++++++++++++++++++++- 6 files changed, 342 insertions(+), 134 deletions(-) create mode 100644 .github/workflows/screenshotter.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 947c790b..b52fc810 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,15 +5,11 @@ on: branches: [ master ] pull_request: branches: [ master ] - pull_request_target: - branches: [ master ] - types: [ labeled ] # 'test screenshots' label on PRs from fork jobs: test: runs-on: ubuntu-latest if: | - github.event_name != 'pull_request_target' && !contains(toJSON(github.event.commits.*.message), '[skip ci]') && !contains(toJSON(github.event.commits.*.message), '[ci skip]') @@ -57,129 +53,4 @@ jobs: - uses: codecov/codecov-action@v1 with: directory: ./coverage/ - - screenshotter_dispatcher: - runs-on: ubuntu-latest - if: | - (github.event_name != 'pull_request_target' || - (github.event.pull_request.head.repo.full_name != 'KaTeX/KaTeX' && - contains(github.event.pull_request.labels.*.name, 'test screenshots'))) && - !contains(toJSON(github.event.commits.*.message), '[skip ci]') && - !contains(toJSON(github.event.commits.*.message), '[ci skip]') - outputs: - matrix: ${{ steps.set-matrix.outputs.result }} - - steps: - - id: set-matrix - uses: actions/github-script@v3 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const SELENIUM_BROWSERS = ["chrome:3.141.59-20201119", "firefox:3.141.59-20201119"]; - const BROWSERSTACK_BROWSERS = [{ - browserName: "safari", - browser_version: "13.1", - os: "OS X", - os_version: "Catalina", - }]; - - const include = []; - - // running selenium doesn't require access to secrets - if (context.eventName !== "pull_request_target") { - include.push(...SELENIUM_BROWSERS.map(browserTag => ({ - browser: browserTag.split(':')[0], - services: {selenium: { - image: `selenium/standalone-${browserTag}`, - ports: ["4444:4444"], - }}, - }))); - } - - // check access to Browserstack crendential secrets - if (context.eventName !== "pull_request" || - context.payload.pull_request.head.repo.full_name === "KaTeX/KaTeX") { - if (context.eventName === "pull_request_target") { - github.issues.removeLabel({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - name: "test screenshots", - }); - } - include.push(...BROWSERSTACK_BROWSERS.map(capabilities => ({ - browser: capabilities.browserName, - services: {}, - browserstack: capabilities, - }))); - } - - return {browser: include.map(b => b.browser), include}; - - screenshotter: - runs-on: ubuntu-latest - needs: screenshotter_dispatcher - strategy: - matrix: ${{ fromJson(needs.screenshotter_dispatcher.outputs.matrix) }} - fail-fast: false - services: ${{ matrix.services }} - - steps: - - uses: actions/checkout@v2 - if: github.event_name != 'pull_request_target' - with: - submodules: recursive - persist-credentials: false # minimize exposure and prevent accidental pushes - - uses: actions/checkout@v2 - if: github.event_name == 'pull_request_target' - with: - # pull_request_target is run in the context of the base repository - # of the pull request, so the default ref is master branch and - # ref should be manually set to the head of the PR - ref: refs/pull/${{ github.event.pull_request.number }}/head - submodules: recursive - - - name: Use Node.js 12.x - uses: actions/setup-node@v1 - with: - node-version: '12' - - - name: Cache dependencies - uses: actions/cache@v2 - with: - path: | - .yarn/cache - .pnp.js - key: yarn-deps-v1-${{ hashFiles('yarn.lock') }} - restore-keys: | - yarn-deps-v1- - - - name: Install dependencies - run: yarn --immutable - env: - YARN_ENABLE_SCRIPTS: 0 # disable postinstall scripts - - - name: Verify screenshots and generate diffs and new screenshots - run: yarn node dockers/screenshotter/screenshotter.js -b ${{ matrix.browser }} --verify --diff --new -c ${{ job.services.selenium.id }} - if: matrix.services.selenium - - name: Verify screenshots and generate diffs and new screenshots - run: yarn node dockers/screenshotter/screenshotter.js -b ${{ matrix.browser }} --verify --diff --new --browserstack --selenium-capabilities '${{ toJson(matrix.browserstack) }}' - if: matrix.browserstack - env: - BROWSERSTACK_USER: ${{ secrets.BROWSERSTACK_USER }} - BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} - - - name: Print Docker logs - run: docker logs ${{ job.services.selenium.id }} - if: always() && matrix.services.selenium - - - uses: actions/upload-artifact@v2 - if: failure() - with: - name: new-${{ matrix.browser }} - path: test/screenshotter/new - - uses: actions/upload-artifact@v2 - if: failure() - with: - name: diff-${{ matrix.browser }} - path: test/screenshotter/diff + timeout-minutes: 3 diff --git a/.github/workflows/screenshotter.yml b/.github/workflows/screenshotter.yml new file mode 100644 index 00000000..c39cb68c --- /dev/null +++ b/.github/workflows/screenshotter.yml @@ -0,0 +1,98 @@ +name: Screenshotter + +on: + push: + branches: [ master ] + pull_request_target: + branches: [ master ] + +jobs: + screenshotter: + runs-on: ubuntu-latest + if: | + !contains(toJSON(github.event.commits.*.message), '[skip ci]') && + !contains(toJSON(github.event.commits.*.message), '[ci skip]') + strategy: + matrix: + browser: [chrome, firefox, safari] + include: + - browser: chrome + image: selenium/standalone-chrome:3.141.59-20201119 + - browser: firefox + image: selenium/standalone-firefox:3.141.59-20201119 + - browser: safari + image: ylemkimon/selenium-proxy:latest + browserstack: + browserName: safari + browser_version: 13.1 + os: OS X + os_version: Catalina + fail-fast: false + services: + selenium: + image: ${{ matrix.image }} + env: + # secrets are not supported in matrix, so put it here and limit to browserstack job + BROWSERSTACK_USER: ${{ matrix.browserstack && secrets.BROWSERSTACK_USER }} + BROWSERSTACK_ACCESS_KEY: ${{ matrix.browserstack && secrets.BROWSERSTACK_ACCESS_KEY }} + + steps: + - uses: actions/checkout@v2 + with: + ref: ${{ github.event_name == 'pull_request_target' && format('refs/pull/{0}/merge', github.event.pull_request.number) || '' }} + submodules: recursive + persist-credentials: false # do not persist credentials + + - name: Use Node.js 14 + uses: actions/setup-node@v1 + with: + node-version: '14' + + - name: Restore cached dependencies # restore only to prevent cache poisoning + uses: ylemkimon/cache-restore@v2 + with: + path: | + .yarn/cache + .pnp.js + key: yarn-deps-v1-${{ hashFiles('yarn.lock') }} + restore-keys: | + yarn-deps-v1- + + - name: Run screenshotter + run: | + TOKEN="$(cat /proc/sys/kernel/random/uuid | sha256sum | head -c 64)" + echo "::add-mask::$TOKEN" + echo "TOKEN=$TOKEN" >> $GITHUB_ENV + echo "::stop-commands::$TOKEN" # stop processing workflow commands + + # run in Docker container + # mount .git readonly to prevent modification + docker run --rm \ + --network ${{ job.services.selenium.network }} \ + -v "$PWD:/code" \ + -v "$PWD/.git:/code/.git:ro" \ + -w /code \ + -e YARN_ENABLE_SCRIPTS=0 \ + -e CI=true \ + node:14 \ + /bin/bash -c 'yarn --immutable && yarn node dockers/screenshotter/screenshotter.js -b ${{ matrix.browser }} --verify --diff --new --katex-ip $HOSTNAME ${{ matrix.browserstack && format('--selenium-proxy http://selenium:4445/build --browserstack --selenium-capabilities ''\''''{0}''\', toJson(matrix.browserstack)) || '--selenium-ip selenium' }}' + echo "::$TOKEN::" + timeout-minutes: 10 + + - name: Print Selenium Docker logs + if: always() + run: | + echo "::stop-commands::$TOKEN" # stop processing workflow commands + docker logs ${{ job.services.selenium.id }} + echo "::$TOKEN::" + + - uses: actions/upload-artifact@v2 + if: failure() + with: + name: new-${{ matrix.browser }} + path: test/screenshotter/new + - uses: actions/upload-artifact@v2 + if: failure() + with: + name: diff-${{ matrix.browser }} + path: test/screenshotter/diff diff --git a/dockers/screenshotter/screenshotter.js b/dockers/screenshotter/screenshotter.js index 75e6acfe..39c5417d 100644 --- a/dockers/screenshotter/screenshotter.js +++ b/dockers/screenshotter/screenshotter.js @@ -8,9 +8,12 @@ const net = require("net"); const os = require("os"); const pako = require("pako"); const path = require("path"); +const got = require("got"); + const selenium = require("selenium-webdriver"); const firefox = require("selenium-webdriver/firefox"); const chrome = require("selenium-webdriver/chrome"); +const seleniumHttp = require("selenium-webdriver/http"); const istanbulLibCoverage = require('istanbul-lib-coverage'); const istanbulLibReport = require('istanbul-lib-report'); @@ -44,6 +47,7 @@ const opts = require("commander") "Port number of the Selenium web driver", 4444, parseInt) .option("--selenium-capabilities ", "Desired capabilities of the Selenium web driver", JSON.parse) + .option("--selenium-proxy ", "Use Selenium proxy if specified") .option("--katex-url ", "Full URL of the KaTeX development server") .option("--katex-ip ", "IP address of the KaTeX development server") .option("--katex-port ", @@ -216,7 +220,8 @@ function startServer() { devServer = wds; katexPort = port; attempts = 0; - process.nextTick(opts.browserstack ? startBrowserstackLocal : tryConnect); + process.nextTick(opts.seleniumProxy ? getProxyDriver + : opts.browserstack ? startBrowserstackLocal : tryConnect); }); server.on("error", function(err) { if (devServer !== null) { // error after we started listening @@ -294,6 +299,28 @@ function buildDriver() { builder.withCapabilities(opts.seleniumCapabilities); } driver = builder.build(); + setupDriver(); +} + +function getProxyDriver() { + got.post(opts.seleniumProxy, { + json: { + browserstack: opts.browserstack, + capabilities: opts.seleniumCapabilities, + seleniumURL, + }, + responseType: 'json', + }).then(({body}) => { + const session = new selenium.Session(body.id, body.capabilities); + const client = Promise.resolve(seleniumURL) + .then(url => new seleniumHttp.HttpClient(url)); + const executor = new seleniumHttp.Executor(client); + driver = new selenium.WebDriver(session, executor); + setupDriver(); + }); +} + +function setupDriver() { driver.manage().timeouts().setScriptTimeout(3000).then(function() { let html = '' + '