From df8e9e6f0108cd06925ea745bb26771ed458c7cb Mon Sep 17 00:00:00 2001 From: Christopher Hiller Date: Wed, 7 Oct 2020 15:52:22 -0700 Subject: [PATCH] run all node.js tests on GHA (#4459) - disables `node_modules` cache on windows cuz its slow - adds annotation support for Mocha tests (via https://npm.im/mocha-github-actions-reporter) and ESLint checks - `::add-path::` is deprecated, but the suggested migration does not work; see [ongoing discussion](https://github.community/t/migration-from-deprecated-add-path-on-windows/136265) - (tangential) add a success threshold to bothersome OC image downloader script; fail production deploys for this reason & do not fail others --- .github/workflows/mocha.yml | 202 +++++++++++++++++++++++++++++++ .github/workflows/windows-ci.yml | 95 --------------- .travis.yml | 61 +--------- docs/_data/supporters.js | 50 ++++++-- package-scripts.js | 11 +- package.json | 3 +- 6 files changed, 253 insertions(+), 169 deletions(-) create mode 100644 .github/workflows/mocha.yml delete mode 100644 .github/workflows/windows-ci.yml diff --git a/.github/workflows/mocha.yml b/.github/workflows/mocha.yml new file mode 100644 index 0000000000..fdaf727232 --- /dev/null +++ b/.github/workflows/mocha.yml @@ -0,0 +1,202 @@ +name: Tests +'on': + push: + pull_request: + types: + - opened + - synchronize + - closed + - reopened + # see https://github.com/bradennapier/eslint-plus-action#handle-forked-prs + schedule: + - cron: '*/15 * * * *' + +jobs: + prepare-commit-msg: + name: Retrive head commit message + runs-on: ubuntu-latest + outputs: + HEAD_COMMIT_MSG: '${{ steps.commitMsg.outputs.HEAD_COMMIT_MSG }}' + steps: + - uses: actions/checkout@v2 + if: github.event_name == 'pull_request' + - name: find commit msg for PR + id: commitMsg + if: github.event_name == 'pull_request' + run: >- + echo "::set-output name=HEAD_COMMIT_MSG::$(git log --no-merges -1 + --oneline)" + + check-skip: + name: Check to skip CI + needs: prepare-commit-msg + runs-on: ubuntu-latest + if: >- + ${{ !contains(github.event.head_commit.message, '[ci skip]') && + !contains(needs.prepare-commit-msg.outputs.HEAD_COMMIT_MSG, '[ci skip]') + }} + steps: + - run: 'echo "${{ github.event.head_commit.message }}"' + + smoke: + name: 'Smoke [Node.js v${{ matrix.node }} / ${{ matrix.os }}]' + needs: check-skip + runs-on: '${{ matrix.os }}' + strategy: + matrix: + os: + - ubuntu-latest + - windows-2019 + node: + - 10 + - 12 + - 14 + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v1 + with: + node-version: '${{ matrix.node }}' + - run: npm install --production + - run: npm run test:smoke + + eslint: + name: ESLint Check + runs-on: ubuntu-latest + needs: smoke + steps: + - uses: actions/checkout@v2 + - uses: bradennapier/eslint-plus-action@v3.4.2 + with: + issueSummary: false + npmInstall: false + + markdown: + name: Markdown Check + runs-on: ubuntu-latest + needs: smoke + steps: + - uses: actions/setup-node@v1 + with: + node-version: 14 + - uses: actions/checkout@v2 + - name: 'Cache node_modules' + uses: actions/cache@v2 + with: + path: '~/.npm' + key: "ubuntu-latest-node-v14-${{ hashFiles('**/package-lock.json') }}" + restore-keys: | + ubuntu-latest-node-v14- + - name: Install Dependencies + run: npm ci --ignore-scripts + - name: 'Check Markdown' + run: npm start lint.markdown + + test-node: + name: 'Node.js [v${{ matrix.node }} / ${{ matrix.os }}]' + needs: smoke + runs-on: '${{ matrix.os }}' + env: + NODE_OPTIONS: '--trace-warnings' + strategy: + matrix: + os: + - ubuntu-latest + - windows-2019 + node: + - 10 + - 12 + - 14 + include: + - os: ubuntu-latest + node: 14 + env: + COVERAGE: 1 + steps: + - name: Cache Growl Installer (Windows) + if: "${{ matrix.os == 'windows-2019' }}" + id: cache-growl + uses: actions/cache@v2 + with: + path: GrowlInstaller + key: '${{ runner.os }}-growl-installer' + restore-keys: | + ${{ runner.os }}-growl-installer + - name: Download Growl Installer (Windows) + if: "${{ matrix.os == 'windows-2019' && steps.cache-growl.outputs.cache-hit != 'true'}}" + run: > + echo "Downloading Growl installer..." + + mkdir GrowlInstaller | out-null + + $seaURL = + "https://github.com/briandunnington/growl-for-windows/releases/download/final/GrowlInstaller.exe" + + $seaPath = "GrowlInstaller\GrowlInstaller.exe" + + $webclient = New-Object Net.WebClient + + $webclient.DownloadFile($seaURL, $seaPath) + + 7z x $seaPath -oGrowlInstaller | out-null + + echo "Done." + - name: Retrieve Growl Installer (Windows) + if: "${{ matrix.os == 'windows-2019' }}" + uses: actions/cache@v2 + with: + path: GrowlInstaller + key: '${{ runner.os }}-growl-installer' + restore-keys: | + ${{ runner.os }}-growl-installer + - name: Add Growl Installer to Path (Windows) + if: "${{ matrix.os == 'windows-2019' }}" + run: 'echo "::add-path::C:\Program Files (x86)\Growl for Windows"' + - name: Install Growl + if: "${{ matrix.os == 'windows-2019' }}" + run: | + echo "Installing Growl..." + cmd /c start /wait msiexec /i GrowlInstaller\Growl_v2.0.msi /quiet + echo "Done." + - name: Start Growl Service (Windows) + if: "${{ matrix.os == 'windows-2019' }}" + run: | + echo "Starting Growl service..." + Start-Process -NoNewWindow Growl + ## Growl requires some time before it's ready to handle notifications + echo "Verifying Growl responding" + Start-Sleep -s 10 + growlnotify test + - name: Install libnotify-bin (Linux) + if: "${{ matrix.os == 'ubuntu-latest' }}" + run: sudo apt-get install libnotify-bin + - uses: actions/setup-node@v1 + with: + node-version: '${{ matrix.node }}' + - uses: actions/checkout@v2 + - name: 'Cache node_modules (Linux)' + if: "${{ matrix.os != 'windows-2019' }}" + uses: actions/cache@v2 + with: + path: '~/.npm' + key: "${{ matrix.os }}-node-v${{ matrix.node }}-${{ hashFiles('**/package-lock.json') }}" + restore-keys: | + ${{ matrix.os }}-node-v${{ matrix.node }}- + - name: Install Dependencies + run: npm ci --ignore-scripts + - name: Install Annotation Support + run: npm install mocha-github-actions-reporter + - name: Run All Node.js Tests + run: npm start test.node + env: + COVERAGE: '${{ matrix.env.COVERAGE }}' + MOCHA_REPORTER: mocha-github-actions-reporter + # this is so mocha-github-actions-reporter can find mocha + NODE_PATH: lib + - name: Generate Coverage Report (Linux + Node.js latest) + if: '${{ matrix.env.COVERAGE }}' + run: npm start coverage-report-lcov + - name: Upload Coverage to Coveralls (Linux + Node.js latest) + if: '${{ matrix.env.COVERAGE }}' + uses: coverallsapp/github-action@master + with: + github-token: '${{ secrets.GITHUB_TOKEN }}' diff --git a/.github/workflows/windows-ci.yml b/.github/workflows/windows-ci.yml deleted file mode 100644 index 3bc1fa7078..0000000000 --- a/.github/workflows/windows-ci.yml +++ /dev/null @@ -1,95 +0,0 @@ -name: Windows CI -on: [push, pull_request] - -jobs: - prepare-commit-msg: - name: Retrive head commit message - runs-on: ubuntu-latest - outputs: - HEAD_COMMIT_MSG: ${{ steps.commitMsg.outputs.HEAD_COMMIT_MSG }} - steps: - - uses: actions/checkout@v1 - if: github.event_name == 'pull_request' - - name: find commit msg for PR - id: commitMsg - if: github.event_name == 'pull_request' - run: echo "::set-output name=HEAD_COMMIT_MSG::$(git log --no-merges -1 --oneline)" - check-skip: - name: Check to skip CI - needs: prepare-commit-msg - runs-on: ubuntu-latest - if: ${{ !contains(github.event.head_commit.message, '[ci skip]') && !contains(needs.prepare-commit-msg.outputs.HEAD_COMMIT_MSG, '[ci skip]') }} - steps: - - run: echo "${{ github.event.head_commit.message }}" - smoke: - name: Smoke on Node.js v${{ matrix.node }} - needs: check-skip - runs-on: windows-2019 - strategy: - matrix: - node: [10, 12, 14] - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v1 - with: - node-version: ${{ matrix.node }} - - run: npm install --production - - run: ./bin/mocha --no-config --reporter spec test/smoke/smoke.spec.js - prepare: - name: Install dependencies - runs-on: windows-2019 - needs: smoke - steps: - - name: Cache Growl Installer - id: cache-growl - uses: actions/cache@v2 - with: - path: GrowlInstaller - key: ${{ runner.os }}-growl-installer - restore-keys: | - ${{ runner.os }}-growl-installer - - name: Download Growl Installer - if: steps.cache-growl.outputs.cache-hit != 'true' - run: | - echo "Installing Growl..." - mkdir GrowlInstaller | out-null - $seaURL = "https://github.com/briandunnington/growl-for-windows/releases/download/final/GrowlInstaller.exe" - $seaPath = "GrowlInstaller\GrowlInstaller.exe" - $webclient = New-Object Net.WebClient - $webclient.DownloadFile($seaURL, $seaPath) - 7z x $seaPath -oGrowlInstaller | out-null - test: - name: Test on Node.js v${{ matrix.node }} - needs: prepare - runs-on: windows-2019 - strategy: - matrix: - node: [10, 12, 14] - steps: - - name: Restore Growl Installer - uses: actions/cache@v2 - with: - path: GrowlInstaller - key: ${{ runner.os }}-growl-installer - restore-keys: | - ${{ runner.os }}-growl-installer - - run: echo "::add-path::C:\Program Files (x86)\Growl for Windows" - - name: Install Growl - run: | - echo "Installing Growl" - cmd /c start /wait msiexec /i GrowlInstaller\Growl_v2.0.msi /quiet - - uses: actions/checkout@v2 - - uses: actions/setup-node@v1 - with: - node-version: ${{ matrix.node }} - - run: npm ci --ignore-scripts - - name: npm start test.node - run: | - echo "Starting Growl service..." - Start-Process -NoNewWindow Growl - ## Growl requires some time before it's ready to handle notifications - echo "Verifying Growl responding" - Start-Sleep -s 5 - growlnotify test - echo "Staring test" - npm start test.node diff --git a/.travis.yml b/.travis.yml index adcf866c6b..e89a15a306 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,19 +4,11 @@ # these are executed in order. each must pass for the next to be run stages: - - smoke # this ensures a "user" install works properly - - precache # warm up cache for default Node.js version - - lint # lint code and docs - test # all tests # defaults language: node_js -node_js: '14' -addons: - apt: - packages: - # Growl - - libnotify-bin +node_js: '12' # `nvm install` happens before the cache is restored, which means # we must install our own npm elsewhere (`~/npm`) before_install: | @@ -28,7 +20,7 @@ before_install: | # avoids bugs around https://github.com/travis-ci/travis-ci/issues/5092 export PATH=~/npm/node_modules/.bin:$PATH # this avoids compilation in most cases (where we don't need it) -install: npm ci --ignore-scripts +install: npm ci cache: directories: - ~/.npm # cache npm's cache @@ -36,23 +28,9 @@ cache: jobs: include: - - script: COVERAGE=1 npm start test.node - after_success: npm start coveralls - name: 'Latest Node.js (with coverage)' - - - &node - script: npm start test.node - node_js: '12' - name: 'Node.js v12' - - - <<: *node - node_js: '10' - name: 'Node.js v10' - - script: npm start test.browser name: 'Browser' node_js: 12 - install: npm ci # we need the native modules here addons: artifacts: paths: @@ -61,43 +39,8 @@ jobs: chrome: stable sauce_connect: true - - stage: lint - script: npm start lint - name: 'JS & Markdown' - - # smoke tests use default npm. - - &smoke - stage: smoke - env: null - before_install: true - install: npm install --production - name: 'Latest Node.js' - script: ./bin/mocha --no-config --reporter spec test/smoke/smoke.spec.js - cache: - directories: - - ~/.npm - - node_modules # npm install, unlike npm ci, doesn't wipe node_modules - - - <<: *smoke - node_js: '12' - name: 'Node.js v12' - - - <<: *smoke - node_js: '10' - name: 'Node.js v10' - - - stage: precache - script: true - name: 'Prime cache' - env: - 'NODE_OPTIONS="--trace-warnings"' notifications: email: false - webhooks: - urls: - # for gitter mochajs/contributors - - secure: rGMGYWBaZgEa9i997jJHKzjI8WxECqLi6BqsMhvstDq9EeTeXkZFVfz4r6G3Xugsk3tFwb/pDpiYo1OK36kA5arUJTCia51u4Wn+c7lHKcpef/vXztoyucvw6/jXdVm/FQz1jztYYbqdyAOWC2BV8gYvg5F8TpK05UGCe5R0bRA= - on_success: change - on_failure: always diff --git a/docs/_data/supporters.js b/docs/_data/supporters.js index 9db8f3dfca..19f1fbb37a 100755 --- a/docs/_data/supporters.js +++ b/docs/_data/supporters.js @@ -51,6 +51,9 @@ const API_ENDPOINT = 'https://api.opencollective.com/graphql/v2'; const SPONSOR_TIER = 'sponsor'; const BACKER_TIER = 'backer'; +// if this percent of fetches completes, the build will pass +const PRODUCTION_SUCCESS_THRESHOLD = 0.8; + const SUPPORTER_IMAGE_PATH = resolve(__dirname, '../images/supporters'); const SUPPORTER_QUERY = `query account($limit: Int, $offset: Int, $slug: String) { @@ -99,7 +102,10 @@ const nodeToSupporter = node => ({ const fetchImage = async supporter => { try { const {avatar: url} = supporter; - const {body: imageBuf} = await needle('get', url); + const {body: imageBuf, headers} = await needle('get', url, {timeout: 2000}); + if (headers['content-type'].startsWith('text/html')) { + throw new TypeError('received html and expected a png; outage likely'); + } debug('fetched %s', url); const canvasImage = await loadImage(imageBuf); debug('ok %s', url); @@ -107,7 +113,7 @@ const fetchImage = async supporter => { width: canvasImage.width, height: canvasImage.height }; - debug('dimensions %s %dw %dh', url, canvasImage.width, canvasImage.height); + // debug('dimensions %s %dw %dh', url, canvasImage.width, canvasImage.height); const filePath = resolve(SUPPORTER_IMAGE_PATH, supporter.id + '.png'); await writeFile(filePath, imageBuf); debug('wrote %s', filePath); @@ -186,8 +192,8 @@ const getSupporters = async () => { (supporters, supporter) => { if (supporter.type === 'INDIVIDUAL') { if (supporter.name !== 'anonymous') { - supporters.backers = [ - ...supporters.backers, + supporters[BACKER_TIER] = [ + ...supporters[BACKER_TIER], { ...supporter, avatar: encodeURI(supporter.imgUrlSmall), @@ -196,8 +202,8 @@ const getSupporters = async () => { ]; } } else { - supporters.sponsors = [ - ...supporters.sponsors, + supporters[SPONSOR_TIER] = [ + ...supporters[SPONSOR_TIER], { ...supporter, avatar: encodeURI(supporter.imgUrlMed), @@ -208,8 +214,8 @@ const getSupporters = async () => { return supporters; }, { - sponsors: [], - backers: [] + [SPONSOR_TIER]: [], + [BACKER_TIER]: [] } ); @@ -220,9 +226,10 @@ const getSupporters = async () => { // Fetch images for sponsors and save their image dimensions await Promise.all([ - ...supporters.sponsors.map(fetchImage), - ...supporters.backers.map(fetchImage) + ...supporters[SPONSOR_TIER].map(fetchImage), + ...supporters[BACKER_TIER].map(fetchImage) ]); + debug('fetched images'); invalidSupporters.forEach(supporter => { supporters[supporter.tier].splice( @@ -230,10 +237,12 @@ const getSupporters = async () => { 1 ); }); + debug('tossed out invalid supporters'); - const backerCount = supporters.backers.length; - const sponsorCount = supporters.sponsors.length; + const backerCount = supporters[BACKER_TIER].length; + const sponsorCount = supporters[SPONSOR_TIER].length; const totalValidSupportersCount = backerCount + sponsorCount; + const successRate = totalValidSupportersCount / invalidSupporters.length; debug( 'found %d valid backers and %d valid sponsors (%d total; %d invalid; %d blocked)', @@ -243,6 +252,23 @@ const getSupporters = async () => { invalidSupporters.length, uniqueSupporters.size - totalValidSupportersCount ); + + if (successRate < PRODUCTION_SUCCESS_THRESHOLD) { + if (process.env.NETLIFY && process.env.CONTEXT !== 'deploy-preview') { + throw new Error( + `Failed to meet success threshold ${PRODUCTION_SUCCESS_THRESHOLD * + 100}% (was ${successRate * + 100}%) for a production deployment; refusing to deploy` + ); + } else { + console.warn( + `WARNING: Success rate of ${successRate * + 100}% fails to meet production threshold of ${PRODUCTION_SUCCESS_THRESHOLD * + 100}%; would fail a production deployment!` + ); + } + } + debug('supporter image pull completed'); return supporters; }; diff --git a/package-scripts.js b/package-scripts.js index 48856cdd39..d238b2d964 100644 --- a/package-scripts.js +++ b/package-scripts.js @@ -23,8 +23,11 @@ function test(testName, mochaParams) { if (process.env.MOCHA_PARALLEL === '0') { mochaParams += ' --no-parallel'; } - if (process.env.TRAVIS) { - mochaParams += ' --color'; // force color in travis-ci + if (process.env.MOCHA_REPORTER) { + mochaParams += ` --reporter=${process.env.MOCHA_REPORTER}`; + } + if (process.env.CI) { + mochaParams += ' --color'; // force color in CI } return `${ process.env.COVERAGE ? coverageCommand : '' @@ -248,6 +251,10 @@ module.exports = { description: 'Send code coverage report to coveralls (run during CI)', hiddenFromHelp: true }, + 'coverage-report-lcov': { + script: 'nyc report --reporter=lcov', + description: 'Write LCOV report to disk (run tests with COVERAGE=1 first)' + }, 'coverage-report': { script: 'nyc report --reporter=html', description: diff --git a/package.json b/package.json index e46259b2dc..f9e24a86f8 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,8 @@ "prepublishOnly": "nps test clean build", "start": "nps", "test": "nps test", - "version": "nps version" + "version": "nps version", + "test:smoke": "node ./bin/mocha --no-config test/smoke/smoke.spec.js" }, "dependencies": { "ansi-colors": "4.1.1",