- Andrew Hagedorn
- Articles
- Exploring Github Actions
Exploring Github Actions
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:
- I get a feel for some technologies that I would't come across in my day to day job
- 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:
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 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 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:
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:
Videos and screenshots linked on my build
I was able to download and view them trivially:
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:
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 deployif: 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
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.
Other Posts
Technology
- React SSR at Scale
- TravisCI, TeamCity, and Kotlin
- The Good and the Bad of Cypress
- Scaling Browser Interaction Tests
- Exploring Github Actions
- Scope and Impact
- Microservices: Back to the Future