1. Andrew Hagedorn
  2. Articles
  3. Exploring Github Actions

Exploring Github Actions

Andrew Hagedorn, September 2020

This website is a mess under the hood and this is mostly intentional. I use it to explore and play around with technologies which has two side effects:

  1. I get a feel for some technologies that I would't come across in my day to day job
  2. The code to generate the HTML is a horrifying Frankenstein's monster of technologies that could be trivially replaced by some out of the box solution

To that end I decided to play with Github Actions.

Exploring

Basics

The welcome to Github Actions example is to use their super linter which sounded like a decent starting point. I first got prettier set up in my repository:

  • Installation: npm install --save-dev prettier
  • Set up files:
    • touch .prettierignore
    • echo {}> .prettierrc.json
  • Linted the code in the repository: npx prettier --write .

From there I modified their example slightly to add the linting step:

name: CI

on:
    pull_request:

jobs:
    linting:
        name: Linting
        runs-on: ubuntu-latest

        steps:
            - name: Checkout code
              uses: actions/checkout@v2

            - name: Set Node.js version
              uses: actions/setup-node@v1
              with:
                  node-version: 12.x

            - name: Install packages
              run: npm install
                
            - name: Run Prettier
              run: npx prettier --check .

Within 3 minutes of setting out I had an action running and passing on my pull request on Github:

A passing action after 3 minutes Passing lint.

I was curious what a failing test run looked like so I changed some indentation and committed it. The failing lint step seemed to adequately indicate what went wrong:

A failing lint step. A failing lint on a yaml file.

Browser Tests

It's great that it was so easy to get started, but I had hoped for something with a little more meat on it. Given my previous experience with getting Cypress to work effectively on TeamCity I decided that would be a reasonable next step.

I first needed to get Cypress set up in my repository:

  • Installation: npm install --save-dev cypress
  • Initialize files by starting the test runner: npx cypress open
  • Set my base URL in cypress.json to point at my local application: { "baseUrl": "http://localhost:3000" }

From there I could write a simple test that passed locally:

it("main image links to the about page", () => {
    cy.visit("/");

    cy.getByDataTest("main-image")
        .should("have.attr", "src")
        .should("match", new RegExp("/images/andrewhagedorn.*.png", "i"));

    cy.getByDataTest("main-image").click();

    cy.url().should("eq", "http://localhost:3000/about");
    cy.get("#about-me").should("exist");
});

This test is very simple; it visits the homepage of my website, validates the main image, and clicks to the about me page.

I then found an existing github action for cypress tests. It's documentation led me to a simple workflow to run my application:

jobs:
    browsertests:
        name: Browser Tests
        runs-on: ubuntu-latest

        steps:
            - name: Checkout code
              uses: actions/checkout@v2

            - name: Cypress run
              uses: cypress-io/github-action@v2
              with:
                  browser: chrome
                  start: npm start
                  wait-on: "http://localhost:3000"
                  wait-on-timeout: 30

            - name: 'Upload Screenshots'
              uses: actions/upload-artifact@v1
              if: failure()
              with:
                name: cypress-screenshots
                path: cypress/screenshots

            - name: 'Upload Videos'
              uses: actions/upload-artifact@v1
              if: failure()
              with:
                name: cypress-videos
                path: cypress/videos

It's a fair bit of yaml, but the structure is pretty simple:

  • Start my application and wait for it to come up
  • Run the tests
  • On failure upload screen shots and videos

I pushed this up to my branch and it passed:

A passing cypress test. A passing test in less than 10 minutes.

Similar to linting I also wanted to see how it behaved with a failing test so I added one:

it("this should fail", () => {
    cy.visit("/");

    cy.getByDataTest("main-image").should("have.attr", "not-there");
});

As expected, this test failed:

A failing cypress test. Sanity...it failed.

Having the details in the UI is nice, but even better was how easy it was to get the screenshots and videos of the failure as artifacts on the build:

Artifacts just worked. Videos and screenshots linked on my build

I was able to download and view them trivially:

The screenshot of the failing test. The screenshot of the failing test

Deploy

Finally, I wanted to see if I could replicate my deploy process. Due to odd historical reasons the markdown that drives this website lives in a seperate repository from the generated HTML that is hosted by Github Pages. However, prior to figuring out how to make that work, I first had to update when my script would run. I had previously set it up to only run on pull requests and my deploy step would require that it also run on master:

name: CI
on:
    pull_request:
    push:
      branches:
        - master

jobs:
   ...

Next I had to deal with my weird multi-repository setup. I had previously been using an existing action to checkout the current repository: actions/checkout@v2. A closer read of the action's documentation showed that it was also possible to checkout multiple repositories, but it required me to add a personal access token as a secret on the repository:

The tab where you add your secrets for your actions Secret secrets

Once I had this configured it was relatively easy to cobble together a simple job to checkout both repositories and run my existing deploy script based on what I had for linting:

deploy:
    name: Deploy
    runs-on: ubuntu-latest

    needs: [linting, browsertests]

    if: success() && github.ref == 'refs/heads/master'

    steps:
        - name: Checkout code
            uses: actions/checkout@v2

        - name: Checkout Deploy Target
            uses: actions/checkout@v2
            with:
            repository: 'username/the-website-repository'
            ref: 'master'
            path: './website'
            token: ${{ secrets.DEPLOY_TOKEN }}

        - name: Set Node.js version
            uses: actions/setup-node@v1
            with:
                node-version: 12.x

        - name: Install packages
            run: npm install
            
        - name: Deploy
            run: npm run deploy

This should look very similar to the previous steps with two notable exceptions:

  • needs: [linting, browsertests]: ensures the previous two steps run prior to deploy
  • if: success() && github.ref == 'refs/heads/master': ensures the previous two steps pass and we only deploy on master

At this point I had a working CI/CD setup and I noticed that I also got some free the bells and whistles like status checks on my repository without any additional work:

Automatic status checks Automatic status checks

Conclusion

Setting up CI/CD for this website with Github Actions turned out to be really easy. I went in expecting a tight integration between my repository and my CI given they are the same platform, but even still I found it incredibly seamless compared to other providers I have used like TeamCity or TravisCI. In my day to day work I am not sure that the cost of migrating to Github Actions would be worth the investment, but overall I was impressed with how easy it was to work with, the documentation, and the level of community support.

© 2024