Angular 8 Deployment within Github Actions Pipeline

Hello everyone,

Here is a small article in order to explain you how I've deployed my Angular front-end to a server with Github actions.

Context: You might know that I am the developer behind cfptime.org and I've had the chance during the holidays to start re-developing the front-end using Angular. If you want to check what it looks like: https://beta.cfptime.org.

I've longer been a CircleCI user but it seems that the free plan is not enough in order to build my Angular project (and trust me, it's nothing ground-breaking, just couple of API calls and that's it).

Anyway, since I was getting blocked with CircleCI, I finally used Github Actions that I heard about couple of weeks/months back and I never had a chance to give it a shot so that was the perfect occasion!

After a bit of Googling, I came across this (well-explained) and recent blog post: https://coryrylan.com/blog/building-angular-cli-projects-with-github-actions.

For the sake of caching, I will copy/paste the final YAML file Cory wrote in his article (which is essentially the one I took most inspiration of):

name: Build
on:
  push:
    branches:
      - master

jobs:
  build:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [12.x]

    steps:
      - uses: actions/checkout@v1

      - name: Cache node modules
        uses: actions/cache@v1
        with:
          path: ~/.npm
          key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
          restore-keys: |
            ${{ runner.os }}-node-

      - name: Node ${{ matrix.node-version }}
        uses: actions/setup-node@v1
        with:
          node-version: ${{ matrix.node-version }}

      - name: npm ci and npm run build
        run: |
          npm ci
          npm run build:ci 

What I needed more with this Github action script was:

  • Build my Angular project in the dist/ folder
  • Copy what I have in my dist/ foder and scp it to my remote host on a specific folder (similar to Continous Delivery)

In order to do so, I installed Angular CLI in my project (responsible for the ng build)

- name: npm ci and npm run build
run: |
    npm ci
    (echo y; echo y; echo y; echo y;) | sudo npm install -g @angular/cli
    npm run build --prod

The next step was to scp the resulting files in the dist/ folder to my remote host.

In order to avoid to display IP addresses, usernames, and/or other variables (such as private keys) to deploy on my server, you can set them as variables within your repo settings and the URL is essentialy: https://github.com/USERNAME/PROJECT/settings/secrets/. Github Actions come with what we can call a marketplace and after a bit of Googling, I came across this plugin https://github.com/garygrossgarten/github-action-scp and its direct link on the marketplace here : https://github.com/marketplace/actions/deploy-via-scp.

<Security Opinion> I'd treat those external plugins/add-ons with a lot of care because they deal with pretty sensitive data (in my case, SSH keys etc.) so review them as much as you can before using one of them inside your pipelines. </Security Opinion>

As soon as you have declared your (sensitive) variables, you're ready to go and here is my example:

- name: Copy folder content recursively to remote
uses: garygrossgarten/github-action-scp@release
with:
    local: dist/cfptime-frontend
    remote: ${{ secrets.SSH_PATH }}
    host: ${{ secrets.SERVER }}
    port: ${{ secrets.SSH_PORT }}
    username: ${{ secrets.SSH_USER }}
    privateKey: ${{ secrets.SSH_PRIVATE_KEY }}

My resulting file build.yml is as follow:

name: Build
on:
  push:
    branches:
      - master

jobs:
  build:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [12.x]

    steps:
      - uses: actions/checkout@v1

      - name: Cache node modules
        uses: actions/cache@v1
        with:
          path: ~/.npm
          key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
          restore-keys: |
            ${{ runner.os }}-node-

      - name: Node ${{ matrix.node-version }}
        uses: actions/setup-node@v1
        with:
          node-version: ${{ matrix.node-version }}

      - name: npm ci and npm run build
        run: |
          npm ci
          (echo y; echo y; echo y; echo y;) | sudo npm install -g @angular/cli
          npm run build --prod

      - name: Copy folder content recursively to remote
        uses: garygrossgarten/github-action-scp@release
        with:
          local: dist/cfptime-frontend
          remote: ${{ secrets.SSH_PATH }}
          host: ${{ secrets.SERVER }}
          port: ${{ secrets.SSH_PORT }}
          username: ${{ secrets.SSH_USER }}
          privateKey: ${{ secrets.SSH_PRIVATE_KEY }}

The last part (but not least) was to have my nginx running on the remote host and ready to serve my pages. I truncated the file since it is serving couple of my websites in order to get you all the exact settings I needed to have.

Basically, I just had to specify the root variable (equivalent with /path/to/my/folder in the code snippet) which has to be the remote folder specified in the remote variable (above in the Github action file). Other than that, you just need to have a specific route for / (which is defined in my location directive)

user www-data;
worker_processes 4;
pid /run/nginx.pid;

events {
    worker_connections 1024;
}

http {

    [... truncated ...]
    server {
        listen 443;
        server_name beta.cfptime.org;
        root /path/to/my/folder/;
        add_header X-Frame-Options DENY;
        add_header X-Content-Type-Options nosniff;
        add_header X-XSS-Protection "1; mode=block";

        location / {
            # First attempt to serve request as file, then
            # as directory, then redirect to index(angular) if no file found.
            try_files $uri $uri/ /index.html;
        }
    }
}

In order to check that everything worked, I just had to go to check if the webpage was live on https://beta.cfptime.org and… that was the case, voilà!

Thanks a lot for reading until here and thanks Cory for the article which helped me a lot (and happy deployment :))