Gleb Bahmutov PRO
JavaScript ninja, image processing expert, software quality fanatic
C / C++ / C# / Java / CoffeeScript / JavaScript / Node / Angular / Vue / Cycle.js / functional programming / testing
* self-managed
NOT WORTH IT
version: 2
jobs:
test:
docker:
- image: cypress/base:10
steps:
- checkout
# restore folders with npm dependencies and Cypress binary
- restore_cache:
keys:
- cache-{{ checksum "package.json" }}
# install npm dependencies and Cypress binary
# if they were cached, this step is super quick
- run:
name: Install dependencies
command: npm ci
- run: npm run cy:verify
# save npm dependencies and Cypress binary for future runs
- save_cache:
key: cache-{{ checksum "package.json" }}
paths:
- ~/.npm
- ~/.cache
# start server before starting tests
- run:
command: npm start
background: true
- run: npm run e2e:record
workflows:
version: 2
build:
jobs:
- test
Docker image
Caching
Caching
Install
run tests
maybe start app
# first, install Cypress, then run all tests (in parallel)
stages:
- build
- test
# to cache both npm modules and Cypress binary we use environment variables
# to point at the folders we can list as paths in "cache" job settings
variables:
npm_config_cache: "$CI_PROJECT_DIR/.npm"
CYPRESS_CACHE_FOLDER: "$CI_PROJECT_DIR/cache/Cypress"
# cache using branch name
# https://gitlab.com/help/ci/caching/index.md
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- .npm
- cache/Cypress
- node_modules
# this job installs NPM dependencies and Cypress
install:
image: cypress/base:10
stage: build
script:
- npm ci
- $(npm bin)/print-env CI
- npm run cy:verify
# all jobs that actually run tests can use the same definition
.job_template: &job
image: cypress/base:10
stage: test
script:
# print CI environment variables for reference
- $(npm bin)/print-env CI
# start the server in the background
- npm run start:ci &
# run Cypress test in load balancing mode, pass id to tie jobs together
- npm run e2e:record -- --parallel --ci-build-id $CI_PIPELINE_ID --group electrons
# actual job definitions
# all steps are the same, they come from the template above
electrons-1:
<<: *job
electrons-2:
<<: *job
electrons-3:
<<: *job
electrons-4:
<<: *job
electrons-5:
<<: *job
pipeline {
agent {
// this image provides everything needed to run Cypress
docker {
image 'cypress/base:10'
}
}
stages {
// first stage installs node dependencies and Cypress binary
stage('build') {
steps {
// there a few default environment variables on Jenkins
// on local Jenkins machine (assuming port 8080) see
// http://localhost:8080/pipeline-syntax/globals#env
echo "Running build ${env.BUILD_ID} on ${env.JENKINS_URL}"
sh 'npm ci'
sh 'npm run cy:verify'
}
}
stage('start local server') {
steps {
// start local server in the background
// we will shut it down in "post" command block
sh 'nohup npm start &'
}
}
// this tage runs end-to-end tests, and each agent uses the workspace
// from the previous stage
stage('cypress parallel tests') {
environment {
// we will be recordint test results and video on Cypress dashboard
// to record we need to set an environment variable
// we can load the record key variable from credentials store
// see https://jenkins.io/doc/book/using/using-credentials/
CYPRESS_RECORD_KEY = credentials('cypress-example-kitchensink-record-key')
// because parallel steps share the workspace they might race to delete
// screenshots and videos folders. Tell Cypress not to delete these folders
CYPRESS_trashAssetsBeforeRuns = 'false'
}
// https://jenkins.io/doc/book/pipeline/syntax/#parallel
parallel {
// start several test jobs in parallel, and they all
// will use Cypress Dashboard to load balance any found spec files
stage('tester A') {
steps {
echo "Running build ${env.BUILD_ID}"
sh "npm run e2e:record:parallel"
}
}
// second tester runs the same command
stage('tester B') {
steps {
echo "Running build ${env.BUILD_ID}"
sh "npm run e2e:record:parallel"
}
}
}
}
}
post {
// shutdown the server running in the background
always {
echo 'Stopping local server'
sh 'pkill -f http-server'
}
}
}
language: node_js
node_js:
# Node 10.3+ includes npm@6 which has good "npm ci" command
- 10.8
cache:
# cache both npm modules and Cypress binary
directories:
- ~/.npm
- ~/.cache
override:
- npm ci
- npm run cy:verify
defaults: &defaults
script:
# ## print all Travis environment variables for debugging
- $(npm bin)/print-env TRAVIS
- npm start -- --silent &
- npm run cy:run -- --record --parallel --group $STAGE_NAME
# after all tests finish running we need
# to kill all background jobs (like "npm start &")
- kill $(jobs -p) || true
jobs:
include:
# we have multiple jobs to execute using just a single stage
# but we can pass group name via environment variable to Cypress test runner
- stage: test
env:
- STAGE_NAME=1x-electron
<<: *defaults
# run tests in parallel by including several test jobs with same name variable
- stage: test
env:
- STAGE_NAME=4x-electron
<<: *defaults
- stage: test
env:
- STAGE_NAME=4x-electron
<<: *defaults
- stage: test
env:
- STAGE_NAME=4x-electron
<<: *defaults
- stage: test
env:
- STAGE_NAME=4x-electron
<<: *defaults
defaults: &defaults
script:
- npm start -- --silent &
- stage: test
<<: *defaults
Single file, no variables, no code isolation
version: 2.1
orbs:
cypress: cypress-io/cypress@1
workflows:
build:
jobs:
- cypress/run
version: 2.1
orbs:
cypress: cypress-io/cypress@1
workflows:
build:
jobs:
- cypress/run
Reusable YAML from Orb registry
version: 2.1
orbs:
cypress: cypress-io/cypress@1
workflows:
build:
jobs:
- cypress/run
Substitute YAML from the orb
$ circleci config process circle.yml
version: 2.1
orbs:
cypress: cypress-io/cypress@1
workflows:
build:
jobs:
- cypress/run
CircleCI Orbs: text expansions + reusable shared configs
- cypress/install:
build: 'npm run build'
- cypress/run:
requires:
- cypress/install
record: true
parallel: true
parallelism: 4
Control behavior using parameters
"build" parameter of "install" job
- cypress/install:
build: 'npm run build'
- cypress/run:
requires:
- cypress/install
record: true
parallel: true
parallelism: 4
Control behavior using parameters
several parameter defined in the "run" job
- cypress/install:
build: 'npm run build'
- cypress/run:
requires:
- cypress/install
record: true
parallel: true
parallelism: 4
Control behavior using parameters
Allow the tool authors to write the specific CI config
name: GitHub Actions Demo
on: [push]
jobs:
Explore-GitHub-Actions:
runs-on: ubuntu-latest
steps:
- run: echo "🎉 The job was automatically triggered by a ${{ github.event_name }} event."
- run: echo "🐧 This job is now running on a ${{ runner.os }} server hosted by GitHub!"
- run: echo "🔎 The name of your branch is ${{ github.ref }} and your repository is ${{ github.repository }}."
- name: Check out repository code
uses: actions/checkout@v3
- name: List files in the repository
run: |
ls ${{ github.workspace }}
GitHub CI system:
Github Actions
CI workflow file:
Github Actions
Reusable CI steps:
Github Actions
- name: Check out repository code
uses: actions/checkout@v3
Actions implemented by GitHub team (nothing extra-ordinary)
Hosted on https://github.com/actions
🤯🤯🤯🤯🤯🤯🤯🤯🤯🤯🤯🤯🤯🤯🤯🤯🤯🤯
GitHub CI system:
Github Actions
CI workflow file:
Github Actions
Reusable CI steps:
Github Actions
GitHub CI system:
Actions CI
CI workflow file:
Workflow file
Reusable CI steps:
Github Actions
Reusable CI workflows:
Reusable workflows
GHA = GitHub Actions and CI
name: GitHub Actions Demo
on: [push]
jobs:
Explore-GitHub-Actions:
runs-on: ubuntu-latest
steps:
- name: Check out repository code
uses: actions/checkout@v3
name: GitHub Actions Demo
on: [push]
jobs:
Explore-GitHub-Actions:
runs-on: ubuntu-latest
steps:
- name: Check out repository code
uses: actions/checkout@v3
name: GitHub Actions Demo
on: [push]
jobs:
Explore-GitHub-Actions:
runs-on: ubuntu-latest
steps:
- name: Check out repository code
uses: actions/checkout@v3
name: GitHub Actions Demo
on: [push]
jobs:
Explore-GitHub-Actions:
runs-on: ubuntu-latest
steps:
- name: Check out repository code
uses: actions/checkout@v3
- name: Check out repository code
uses: actions/checkout@v3
name: 'Checkout'
description: 'Checkout a Git repository at a particular version'
inputs:
repository:
description: 'Repository name with owner. For example, actions/checkout'
default: ${{ github.repository }}
ref:
description: >
The branch, tag or SHA to checkout. When checking out the repository that
triggered a workflow, this defaults to the reference or SHA for that
event. Otherwise, uses the default branch.
token:
...
runs:
using: node16
main: dist/index.js
post: dist/index.js
steps:
# Reference a specific commit
- uses: actions/setup-node@74bc508
# Reference the major version of a release
- uses: actions/setup-node@v1
# Reference a minor version of a release
- uses: actions/setup-node@v1.2
# Reference a branch
- uses: actions/setup-node@master
name: main
on: [push]
jobs:
build-and-test:
runs-on: ubuntu-latest
name: Build and test
steps:
- uses: actions/checkout@v3
- uses: bahmutov/npm-install@v1
- run: npm t
list of steps: shell commands, 3rd party actions
name: main
on: [push]
jobs:
build-and-test:
runs-on: ubuntu-latest
name: Build and test
steps:
- name: Check out code 🛎
uses: actions/checkout@v3
- name: Install dependencies 📦
uses: bahmutov/npm-install@v1
- name: Run tests 🧪
run: npm t
Tip: give each step a good name
- uses: bahmutov/npm-install@v1
with:
working-directory: app1
# Do not detect and use a lock file
- uses: bahmutov/npm-install@v1
with:
useLockFile: false
# install prod dependencies only
- uses: bahmutov/npm-install@v1
env:
NODE_ENV: production
# install with some token
- uses: bahmutov/npm-install@v1
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
https://github.com/<org name>/<repo name>/settings/secrets/actions
{
"scripts": {
"build": "ncc build"
},
"dependencies": {
"@actions/core": "^1.10.0",
"@actions/exec": "^1.0.1",
"@actions/github": "^2.2.0",
"@actions/io": "^1.0.1",
"@actions/tool-cache": "^1.1.2",
"uuid": "^3.3.3"
},
"devDependencies": {
"@zeit/ncc": "^0.20.5"
}
}
A reusable action must be just a single JavaScript file that is ready to go*
* yes, you can make any action using Docker container, but who cares?!
runs:
using: node16
main: dist/index.js
post: dist/index.js
Making a JS action
- uses: actions/checkout@v3
# pick the Node version to use and install it
# https://github.com/actions/setup-node
- uses: actions/setup-node@v3
with:
node-version: 16
- uses: bahmutov/npm-install@v1
Design principles: have each action do 1 thing well
changed files
import core from '@actions/core'
core.setOutput('CircleCIWorkflowUrl', url)
- name: Trigger the deployment
run: npx trigger-circleci-pipeline
id: trigger
- name: Print the workflow URL
run: echo Workflow URL ${{ steps.trigger.outputs.CircleCIWorkflowUrl }}
const url = getWebAppUrl(w)
if (process.env.GITHUB_STEP_SUMMARY) {
const summary = `CircleCI workflow ${w.name} URL: ${url}\n`
writeFileSync(process.env.GITHUB_STEP_SUMMARY, summary, {
flag: 'a+',
})
}
append to process.env.GITHUB_STEP_SUMMARY file
- uses: cypress-io/github-action@v4
- uses: actions/upload-artifact@v2
if: failure()
with:
name: cypress-screenshots
path: cypress/screenshots
- uses: actions/upload-artifact@v2
if: always()
with:
name: cypress-videos
path: cypress/videos
jobs:
unit
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: bahmutov/npm-install@v1
- run: npm test
e2e:
needs: unit
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: cypress-io/github-action@v3
serial
install, caching, running E2E tests
jobs:
test:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v3
- uses: cypress-io/github-action@v4
test-cypress-v9:
...
release:
needs: [test, test-cypress-v9]
runs-on: ubuntu-20.04
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v3
- run: npm install semantic-release
- name: Semantic Release 🚀
uses: cycjimmy/semantic-release-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
Workflow execution
Workflow summary
jobs:
test:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v3
- uses: cypress-io/github-action@v4
test-cypress-v9:
...
release:
needs: [test, test-cypress-v9]
runs-on: ubuntu-20.04
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v3
- run: npm install semantic-release
- name: Semantic Release 🚀
uses: cycjimmy/semantic-release-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
Token for this repo only
created automagically 🪄
name: badges
on:
push:
# update README badge only if the README file changes
# or if the package.json file changes, or this file changes
branches:
- master
paths:
- README.md
- package.json
- .github/workflows/badges.yml
schedule:
# update badges every night
# because we have a few badges that are linked
# to the external repositories
- cron: '0 4 * * *'
jobs:
badges:
name: Badges
runs-on: ubuntu-20.04
steps:
- name: Checkout 🛎
uses: actions/checkout@v3
- name: Update version badges 🏷
run: npx -p dependency-version-badge update-badge cypress
- name: Commit any changed files 💾
uses: stefanzweifel/git-auto-commit-action@v4
with:
commit_message: Updated badges
branch: master
file_pattern: README.md
The "badged" workflow updated the README.md and pushed the change
Note: commits / events from GitHub Actions CI cannot trigger other GH Actions workflows to avoid infinite loops
name: Prettier
on: [push]
jobs:
build:
name: Prettier
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: bahmutov/npm-install@v1
- run: npm run format
- name: Commit any changed files 💾
uses: stefanzweifel/git-auto-commit-action@v4
with:
commit_message: Formatted the code
file_pattern: 'src/**/*.*'
# after checking out
# and testing it, building the site, etc
- name: Deploy 🚀
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./public
- name: Set commit status 📫
run: |
curl --request POST \
--url https://api.github.com/repos/${{ github.repository }}/statuses/${{ github.sha }} \
--header 'authorization: Bearer ${{ secrets.GITHUB_TOKEN }}' \
--header 'content-type: application/json' \
--data '{
"state": "success",
"description": "REST commit status",
"context": "a test"
}'
Details about the current repo and source reference
Main large objects
Example github context
- run: echo SHA ${{ github.sha }}
name: Parallel tests
on: [push]
jobs:
cypress-run:
strategy:
matrix:
machines: [1, 2, 3]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: cypress-io/github-action@v1
with:
record: true
parallel: true
env:
CYPRESS_RECORD_KEY: ${{ secrets.cy_token }}
Matrices of jobs
name: Parallel tests
on: [push]
jobs:
cypress-run:
strategy:
matrix:
machines: [1, 2, 3]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: cypress-io/github-action@v1
with:
record: true
parallel: true
env:
CYPRESS_RECORD_KEY: ${{ secrets.cy_token }}
Matrices of jobs
Trigger events
name: The best workflow
on:
push
pull_request
schedule
...
on:
pull_request:
types: [opened]
jobs:
pr-labeler:
runs-on: ubuntu-latest
steps:
- uses: TimonVS/pr-labeler-action@v3
with:
configuration-path: .github/pr-labeler.yml
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Labeled PRs based on branch names
Labeled PRs based on branch names
Trigger workflow on PR edit
name: pr
on:
pull_request:
types:
- edited
jobs:
rerun-tests:
steps:
- uses: bahmutov/should-run-github-action@v1
id: check-pr
env:
GITHUB_EVENT: ${{ toJson(github.event) }}
- if: ${{ steps.check-pr.outputs.shouldRun }}
run: npm test
Trigger workflow on PR edit
# .github/PULL_REQUEST_TEMPLATE.md
To re-run the tests, pick the tags above then click the checkbox below
- [ ] re-run the tests
name: pr
on:
pull_request:
types:
- edited
jobs:
rerun-tests:
steps:
- uses: bahmutov/should-run-github-action@v1
id: check-pr
env:
GITHUB_EVENT: ${{ toJson(github.event) }}
- if: ${{ steps.check-pr.outputs.shouldRun }}
run: npm test
Trigger workflow on PR edit
Manual triggers and UI
name: coupon tests
on:
# run these tests either manually or on schedule
workflow_dispatch:
description: Trigger coupon web tests
schedule:
# run these tests every day
- cron: '0 23 * * *'
jobs:
...
Manual dispatch parameters!
name: trigger-tests
on:
workflow_dispatch:
inputs:
# these inputs will be shown to the user on GitHub Actions page
# and the user can simply check off the tags to run
# NOTE: GitHub Action workflows have ten inputs max
sanity:
description: Run the tests tagged "@sanity", small set
required: false
type: boolean
all:
description: Run all the tests
required: false
type: boolean
# tests for individual features - the user will need to type the tags
# comma-separated in order to avoid hitting then ten workflow input limit
testTags:
description: |
Other test tags to run, comma-separated. Includes @profile, @signup,
...
# a few other utility params
testUrl:
description: The base URL to run tests against
required: false
type: string
machines:
description: Number of machines to use
required: false
type: integer
default: 8
...
Manual dispatch parameters!
- run: |
# collect all input parameters into one string
TAGS=
if [[ "${{ github.event.inputs.sanity }}" == "true" ]]; then
TAGS="@sanity"
fi
if [[ "${{ github.event.inputs.testTags }}" != "" ]]; then
TAGS="$TAGS,${{ github.event.inputs.testTags }}"
fi
echo "Collected tags: $TAGS"
echo "Number of machines to use: ${{ github.event.inputs.machines }}"
...
trigger tests
Manual dispatch parameters!
🎯 Any engineer can run end-to-end tests against any preview environment and observe the results
Reusable workflows
name: End-to-end tests
on: [push]
jobs:
cypress-run:
runs-on: ubuntu-20.04
steps:
- name: Checkout
uses: actions/checkout@v3
# Install NPM dependencies, cache them correctly
# and run all Cypress tests
- name: Cypress run
uses: cypress-io/github-action@v4
Same thing again and again
Reusable workflows
name: ci
on: [push]
jobs:
test:
uses: bahmutov/cypress-workflows/.github/workflows/standard.yml@v1
single testing machine
name: ci
on: [push]
jobs:
test:
uses: bahmutov/cypress-workflows/.github/workflows/parallel.yml@v1
with:
n: 3
secrets:
recordKey: ${{ secrets.CYPRESS_RECORD_KEY }}
3 testing machines in parallel
Reusable workflows
My workflow A
My workflow B
Reusable
workflow X
GH Action 1
GH Action 2
GH Action 3
Isolation and reuse for your GitHub workflows and actions
By Gleb Bahmutov
Writing continuous integration YML scripts is a cumbersome and awkward process. What if there was a better way? GitHub Actions let you write JavaScript code to build, test, and deploy your applications. Even better, you don't have to write actions yourself. You can reuse actions written by others. Plus, integration with the GitHub repo security lets your CI code easily contribute back to the repository or trigger other steps. Presented at DevOpsCon NYC September 2023, 30 minutes
JavaScript ninja, image processing expert, software quality fanatic