Table of Contents

Overview

One of the most important skills for becoming an experienced software developer is the automation of the repeatable tasks and I believe the most important one of them is the deployment process itself which includes merging, testing and deploying the code in the article, I want to show to efficiently build a CI/CD pipeline that does all of that

I found GitHub actions to be extremely powerful and easy to use and the fact that GitHub is widely used by developers made it ideal to choose it as part of this pipeline

  • Prerequisites:

About the example app

For demonstration purposes I have built a simple Todo list app with React and Typescript, I have used Vite as a development server and build tool because its simple, fast, and most importantly easily used with Vitest for testing

App structure

 📂 Todo-App
 ┣ 📂node_modules
 ┣ 📂src
 ┃ ┣ 📂components
 ┃ ┃ ┗ 📜TodoPanel.tsx
 ┃ ┣ 📂utils
 ┃ ┃ ┗ 📜test-utils.tsx
 ┃ ┣ 📜App.tsx
 ┃ ┣ 📜App.spec.ts
 ┃ ┣ 📜index.css
 ┃ ┣ 📜Types.ts
 ┣ 📜App.yaml
 ┣ 📜index.html
 ┣ 📜postcss.config.js
 ┣ 📜tailwind.config.js
 ┣ 📜tsconfig.json
 ┣ 📜vite.config.ts
 ┣ 📜.gitignore
 ┗ 📜package.json

Getting our hands dirty with the tests

I have already installed and configured Vitest and enzyme to save us time and to keep this post focused on the CI/CD pipeline, the test is straightforward.


import { it, describe, expect, beforeEach, vi } from "vitest";
import App from "./App";
import TodoPanel from "./components/TodoPanel";
import { shallow } from "enzyme";
import { render, screen, userEvent, fireEvent } from "./utils/test-utils";
import "@testing-library/jest-dom/extend-expect";

describe("Test App", () => {
  let wrapper: any;

  beforeEach(() => {
    wrapper = shallow(<App />);
  });

  it("Check if the component is mounted", () => {
    expect(App).toBeTruthy();
  });

  it("Check if the TodoPanel has been rendered", () => {
    expect(wrapper.find(TodoPanel).length).toBe(1);
  });

  it("Check if the button has been rendered", () => {
    expect(wrapper.find("button").length).toBe(1);
  });

  it("Check if todo item is added", async () => {
    render(<App />);
    const input = screen.getByTestId("todo-input") as HTMLInputElement;
    fireEvent.change(input, {
      target: { value: "Testing" },
    });
    userEvent.click(screen.getByRole("button"));
    expect(await screen.getByText(/Testing/i)).toBeInTheDocument();
  });
});

Now we should be able to run vitest and the all test cases shloud pass

npx vitest

Manual Deployment

If everything goes well now its the time to deploy the project, first lets start by setting up the app engine configurations, create a file in the root of the project and name it app.yaml and put the following


env: standard
runtime: nodejs16

handlers:
  - url: /assets
    static_dir: assets

  - url: /(.*\.(js|css))$
    static_files: assets/\1
    upload: assets/.*\.(json|ico|js|css)$
  - url: .*
    static_files: index.html
    upload: index.html

We are setting the environment to standard and the runtime to nodejs16 you can learn more about app engine environments by click here,And the handlers are URL patterns and descriptions of how they should be handled, for exapmle am making assets a static dirctory and mapping very request for a JS or CSS file to it, and finally I’m mapping any other request to index.html file

Now if you didn’t install gcloud yet now is a good time, click here for instructions.

First login to your account

gcloud auth login

Select the account and allow Google SDK to use your account you will notice the project name in the terminal, if it’s not the project that you want to use you can change it with this command.

gcloud config set project PROJECT_ID

Now that we are authenticated we can deploy the app with:

gcloud app deploy

After the deploying finish, you will see the deployed app URL, copy it and past it in the browser or hold control and click on it

Setting up GitHub actions


name: CI

# Controls when the workflow will run
on:
  # Triggers the workflow on push or pull request events but only for the master branch
  push:
    branches: [master]
  pull_request:
    branches: [master]

  # Allows you to run this workflow manually from the Actions tab
  workflow_dispatch:

# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
  test:
    # The type of runner that the job will run on
    runs-on: ubuntu-latest

    # Steps represent a sequence of tasks that will be executed as part of the job
    steps:
      # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
      - uses: actions/checkout@v2
      - uses: pnpm/action-setup@v2.1.0
        with:
          version: 6.0.2
          run_install: |
            - recursive: true
              args: [--frozen-lockfile, --strict-peer-dependencies]
      - name: Run vitest
        run: pnpm run test

  deploy:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2

      - uses: pnpm/action-setup@v2.1.0
        with:
          version: 6.0.2
          run_install: |
            - recursive: true
              args: [--frozen-lockfile, --strict-peer-dependencies]
      - name: Build the project
        run: pnpm run build

      - name: copy app.yaml file to the build directory
        run: cp app.yaml dist

      - name: cd to the build directory and deploy the project
        run: cd dist

      - uses: google-github-actions/auth@v0
        with:
          credentials_json: "${{ secrets.GCP_SA_KEY }}"

      - uses: google-github-actions/deploy-appengine@main
        with:
          working_directory: dist
          version: 20220313t185328

We have two jobs the first one installs PNPM and run npx vitest, and the second one is where the magic is happening so let’s explain it deeper.

Before deploying we need to make sure the test is finished, that is why we added needs: test to the job, then we install PNPM and build the project.

If you notice that the app.yaml is at the root of the project that is why after building the project we need to copy app.yaml to dist which is the build directory.

Finally, inside the dist directory, we simply use google-github-actions/auth@v0 action to authenticate and google-github-actions/deploy-appengine@main to deploy the project

Conclusion

CI/CD supercharge software development flow and is very important to prevent problems by abstracting the deployment and stop deploying bad code by adding tests to the pipeline, Github actions provide a clean and easy way to build them directly into the project.