Is There Anything GitHub Actions Cannot Do?!

Gleb Bahmutov

@bahmutov

Join others to fight the climate crisis

Speaker: Gleb Bahmutov PhD

C / C++ / C# / Java / CoffeeScript / JavaScript / Node / Angular / Vue / Cycle.js / functional programming / testing

Gleb Bahmutov

Sr Director of Engineering

Agenda

  • The (sad) state of CI
  • YAML reuse
  • JavaScript CI config
  • GitHub Actions examples
    • installing dependencies
    • testing
    • code style, badges, etc

CI setup: dream

Reality ๐Ÿคฏ

Lots of CI providers

  • Travis
  • CircleCI
  • BuildKite
  • Semaphore
  • AppVeyor
  • GitLab
  • Codeship
  • AzureCI
  • Shippable
  • Jenkins*
  • TeamCity*

* self-managed

NOT WORTH IT

Every CI config file

  • git checkout
  • install dependencies and tools
  • build code
  • start app
  • test app
  • report results

Every CI config file

  • git checkout
  • install dependencies and tools
  • build code
  • start app
  • test app
  • report results

caching!!!

Every CI config file

  • git checkout
  • install dependencies and tools
  • build code
  • start app
  • test app
  • report results

caching!!!

when is it ready?

how to stop it?

Every CI config file

  • git checkout
  • install dependencies and tools
  • build code
  • start app
  • test app
  • report results

caching!!!

when is it ready?

how to stop it?

in parallel?

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

Typical front-end web app testing using Cypress

defaults: &defaults
  working_directory: ~/app
  docker:
    - image: cypress/browsers:chrome67

version: 2
jobs:
  build:
    <<: *defaults
    steps:
      - checkout
      # find compatible cache from previous build,
      # it should have same dependencies installed from package.json checksum
      - restore_cache:
          keys:
            - cache-{{ .Branch }}-{{ checksum "package.json" }}
      - run:
          name: Install Dependencies
          command: npm ci
      # run verify and then save cache.
      # this ensures that the Cypress verified status is cached too
      - run: npm run cy:verify
      # save new cache folder if needed
      - save_cache:
          key: cache-{{ .Branch }}-{{ checksum "package.json" }}
          paths:
            - ~/.npm
            - ~/.cache
      - run: npm run types
      - run: npm run stop-only
      # all other test jobs will run AFTER this build job finishes
      # to avoid reinstalling dependencies, we persist the source folder "app"
      # and the Cypress binary to workspace, which is the fastest way
      # for Circle jobs to pass files
      - persist_to_workspace:
          root: ~/
          paths:
            - app
            - .cache/Cypress

  4x-electron:
    <<: *defaults
    # tell CircleCI to execute this job on 4 machines simultaneously
    parallelism: 4
    steps:
      - attach_workspace:
          at: ~/
      - run:
          command: npm start
          background: true
      # runs Cypress test in load balancing (parallel) mode
      # and groups them in Cypress Dashboard under name "4x-electron"
      - run: npm run e2e:record -- --parallel --group $CIRCLE_JOB

workflows:
  version: 2
  # this workflow has 4 jobs to show case Cypress --parallel and --group flags
  # "build" installs NPM dependencies so other jobs don't have to
  #   โ”” "1x-electron" runs all specs just like Cypress pre-3.1.0 runs them
  #   โ”” "4x-electron" job load balances all specs across 4 CI machines
  #   โ”” "2x-chrome" load balances all specs across 2 CI machines and uses Chrome browser
  build_and_test:
    jobs:
      - build
      # this group "4x-electron" will load balance all specs
      # across 4 CI machines
      - 4x-electron:
          requires:
            - build

Parallel config is ... more complicated

- install + run jobs

- workspace

- parallel flags

Typical front-end web app testing using Cypress

Copy / paste / tweak until CI passes ๐Ÿ™

# 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

Different CIs

Copy / paste / tweak until CI passes ๐Ÿ˜ก

defaults: &defaults
  script:
    - npm start -- --silent &
    
- stage: test
    <<: *defaults

YAML code "reuse"

Single file, no variables, no code isolation

Let's Make YAML Better

CircleCI Orbs

  • reusable pieces of YAML
  • parameters
  • a central registry of orbs

E2E Testing With Cypress CircleCI Orb

version: 2.1
orbs:
  cypress: cypress-io/cypress@1
workflows:
  build:
    jobs:
      - cypress/run

E2E Testing With Cypress CircleCI Orb

version: 2.1
orbs:
  cypress: cypress-io/cypress@1
workflows:
  build:
    jobs:
      - cypress/run

Reusable YAML from Orb registry

E2E Testing With Cypress CircleCI Orb

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

Reusable YAML Orbs

Allow the tool authors to write the specific CI config

Still hard to write more complex CI scripts

  • Run this command, but only for pull requests matching "fix-..."
  • Copy files matching this pattern (for all OSes)
  • Upload test results to 3rd party system

GitHub Actions ๐Ÿฅ‡

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 }}

How to pick a good name: GitHub / MS edition

GitHub CI system:

Github Actions

CI workflow file:

Github Actions

Reusable CI steps:

Github Actions

How to pick a good name: GitHub / MS edition

- name: Check out repository code
  uses: actions/checkout@v3

Actions implemented by GitHub team (nothing extra-ordinary)

Hosted on https://github.com/actions

๐Ÿคฏ๐Ÿคฏ๐Ÿคฏ๐Ÿคฏ๐Ÿคฏ๐Ÿคฏ๐Ÿคฏ๐Ÿคฏ๐Ÿคฏ๐Ÿคฏ๐Ÿคฏ๐Ÿคฏ๐Ÿคฏ๐Ÿคฏ๐Ÿคฏ๐Ÿคฏ๐Ÿคฏ๐Ÿคฏ

How to pick a good name: GitHub / MS edition

GitHub CI system:

Github Actions

CI workflow file:

Github Actions

Reusable CI steps:

Github Actions

๐Ÿค”

๐Ÿคท

My Names

GitHub CI system:

Actions CI

CI workflow file:

Workflow file

Reusable CI steps:

Github Actions

Reusable CI workflows:

Reusable workflows

name: GitHub Actions Demo
on: [push]
jobs:
  Explore-GitHub-Actions:
    runs-on: ubuntu-latest
    steps:
      - name: Check out repository code
        uses: actions/checkout@v3
  • run this workflow on every Git push
  • a single workflow can have multiple jobs
  • run this workflow in Ubuntu container
  • check out the code using 3rd party GH action
- name: Check out repository code
  uses: actions/checkout@v3

Org / repo @ tag | commit

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  

Org / repo @ tag | commit

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

Control actions through parameters

# Do not detect and use a lock file
- uses: bahmutov/npm-install@v1
  with:
    useLockFile: false
# Use a custom install command
- uses: bahmutov/npm-install@v1
  with:
    install-command: yarn --frozen-lockfile --silent
# install prod dependencies only
- uses: bahmutov/npm-install@v1
  env:
    NODE_ENV: production

Pass environment variables

# install with some token
- uses: bahmutov/npm-install@v1
  env:
    NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

Pass CI secrets as env vars

Pass CI secrets as env vars

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

console.log(`::set-output name=CircleCIWorkflowUrl::${url}`)

A job can output values by simply printing with "::set-output" prefix

- name: Trigger the deployment
  run: npx trigger-circleci-pipeline
  id: trigger

- name: Print the workflow URL
  run: echo Workflow URL ${{ steps.trigger.outputs.CircleCIWorkflowUrl }}

Simple job output

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

Tip: Use Markdown

Save Artifacts

- 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:
  node16:
    runs-on: ubuntu-latest
    steps:
	  - uses: actions/checkout@v3
	  - uses: actions/setup-node@v3
	    with:
    	  node-version: 16
	  - uses: bahmutov/npm-install@v1
      - run: npm test
      
  node18:
    runs-on: ubuntu-latest
    steps:
	  - uses: actions/checkout@v3
	  - uses: actions/setup-node@v3
	    with:
    	  node-version: 18
	  - uses: bahmutov/npm-install@v1
      - run: npm test

Test on different Node versions

in parallel

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

Unit then E2E tests

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 }}

Test then release

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 }}

Test then release

Token for this repo only

created automagically ๐Ÿช„

GitHub Actions CI integration with GitHub repo security is ๐Ÿ˜˜ and ๐Ÿ’ช

README version badges

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

README version badges

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/**/*.*'

Format code

# 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

Publish GitHub Pages

Publish GitHub Pages

- 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"
      }'

Post commit status

Details about the current repo and source reference

Contexts

Main large objects

  • github
  • env
  • steps
  • matrix

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

Balance tests on each OS

parallel-runs-across-platforms:
  strategy:
    matrix:
      os: ['ubuntu-latest', 'windows-latest', 'macos-latest']
      machines: [1, 2]
  runs-on: ${{ matrix.os }}
  steps:
    - uses: actions/checkout@v1
    - uses: cypress-io/github-action@v1
      with:
        record: true
        parallel: true
        group: Parallel 2x on ${{ matrix.os }}
      env:
        CYPRESS_RECORD_KEY: ${{ secrets.cy_token }}

Balance tests on each OS

parallel-runs-across-platforms:
  strategy:
    matrix:
      os: ['ubuntu-latest', 'windows-latest', 'macos-latest']
      machines: [1, 2]
  runs-on: ${{ matrix.os }}
  steps:
    - uses: actions/checkout@v1
    - uses: cypress-io/github-action@v1
      with:
        record: true
        parallel: true
        group: Parallel 2x on ${{ matrix.os }}
      env:
        CYPRESS_RECORD_KEY: ${{ secrets.cy_token }}

recorded parallel runs

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

Play Wordle

3:01am

Play Worlde on CI โค๏ธ

name: hint
# run the workflow every morning
# or when we trigger the workflow manually
on:
  workflow_dispatch:
    inputs:
      email:
        description: Email to send the hint to
        type: string
        default: ''
        required: false
      hints:
        description: Number of letters to reveal
        type: integer
        default: 1
  schedule:
    # UTC time
    - cron: '0 7 * * *'
jobs:
  hint:
    runs-on: ubuntu-20.04
    steps:
      - name: Checkout ๐Ÿ›Ž
        uses: actions/checkout@v3

      - name: Cypress run ๐Ÿงช
        #  https://github.com/cypress-io/github-action
        uses: cypress-io/github-action@v3
        with:
          spec: 'cypress/integration/email-hint.js'
          env: 'hints=${{ github.event.inputs.hints }}'
        env:
          SENDGRID_API_KEY: ${{ secrets.SENDGRID_API_KEY }}
          SENDGRID_FROM: ${{ secrets.SENDGRID_FROM }}
          WORDLE_HINT_EMAIL: ${{ github.event.inputs.email || secrets.WORDLE_HINT_EMAIL }}

Trigger tests with a "/" PR comment

Developer comments "/cypress tags=..."

Results are posted as a commit status checks

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: Parallel Cypress Tests
on: [push]
jobs:
  test:
    name: Cypress run
    runs-on: ubuntu-20.04
    strategy:
      fail-fast: false
      matrix:
        containers: [1, 2, 3]
    steps:
      - name: Checkout
        uses: actions/checkout@v3
      - name: Cypress run
        uses: cypress-io/github-action@v4
        with:
          record: true
          parallel: true
          group: 'Actions example'
        env:
          CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

ughh, syntax is powerful but verbose

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

  • Workflows are in the same repo
  • Called workflow is in a public repo

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

Summary: GitHub Actions are

  • ๐Ÿฆธ powerful
  • ๐Ÿฅฐ joy to use

Learn more about GH Actions

๐Ÿ‘ Thank You ๐Ÿ‘

Gleb Bahmutov

@bahmutov

Is there anything GitHub Actions cannot do?!

By Gleb Bahmutov

Is there anything GitHub Actions cannot do?!

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 but reuse the actions written by others. Plus the integration with the GitHub repo security lets your CI code easily contribute back to the repository or trigger other steps.

  • 1,252