Speeding Up Angular Scully Builds in GitHub Actions

In this article, we are going to learn how you can speed your Scully builds by re-using Angular build artifacts in GitHub Actions. In order to statically build your Angular website with Scully, first you have to do the Angular build and then use the build artifacts to generate a statically generated site using Scully.

It is common for most websites, that content can change without the source code of your website changing. Therefore, it can be wasteful to run an Angular build every time your website content changes.

Normally, Angular builds time are decent. But due to a number of factors, Angular builds could slow down, like in my case, running purge CSS against Tailwindcss extends the build time to over 7 minutes. Add everything else together, and my GitHub Actions would take over 12 minutes.

Using GitHub Releases

First, we are going to need a place to store our Angular build artifacts. GitHub releases are a nice way, as it allows you to have a long-term storage of your artifacts that you can use anytime you want. This combined with npm version means ones you have your features ready; you can cut a release that will be used by subsequent builds as you continue to work on other features and/or improvements.

So, we are going to build our workflow to have two jobs, the first job will take care of building our Angular app, and creating a release and uploading our build artifacts to the release. While the second job will take care of Scully builds using the latest artifacts stored in GitHub releases and publishing our website to our hosting platform.

Whenever a new tag is added to the repository, we will create a release with the version no. of the tag and upload our angular builds to that release.

Building our Angular App

Listening to Tags

First, we will need to trigger our GitHub workflow every time a new tag is created. We will be using tags to create release version. This will allow us to use npm version to create new build artifacts for us to use during the publishing process.

on:
  push:
    tags:
      - "*"
NB: In the publish our blog section, we will modify this section to listen to repository_dispatch, which we will use with webhooks to trigger the workflow when events outside our repository like blog post published occur, you can learn more here.

We will limit this job to only run when a new tag is created using startsWith(github.ref, 'refs/tags/'). This will allow us to utilize the same workflow file for building and publishing, with them being two separate jobs.

jobs:
  build:
    if: startsWith(github.ref, 'refs/tags/')
    runs-on: ubuntu-latest

Installing NPM Packages

Next, we will need to install NPM packages before we can build our angular app. In this case, we are using yarn but feel free to use your favorite package manager. We will start by checking out (git checkout) our repository. After that, we will then setup NodeJS and finally run yarn install to install our NPM packages.

steps:
  - uses: actions/checkout@v1
  - name: Setup Node
    uses: actions/setup-node@v1
    with:
      node-version: 12.x
  - name: yarn install
    run: yarn install

Building Angular Project

And then, we can add a step to run yarn build:prod to build our Angular app in production.

- name: yarn build
  run:  yarn build:prod

Creating a Release and Uploading Artifacts

Now that we have built our project, we are going to do two things next. We will zip the build artefacts and then create a release and upload our zipped artifact to the releases. We will use papeloto/action-zip action to zip the files:

- uses: papeloto/action-zip@v1
  with:
    files: "./dist/webapp/"
    dest: webapp.zip
Replace webapp with the output directory of your angular project build.

And then, we are going to create a GitHub release and upload the above zipped artifact to the GitHub release. We will be using ncipollo/release-action action, to accomplish this as shown below.

- name: Push Build to Releases
  uses: ncipollo/release-action@v1
  with:
    artifacts: "webapp.zip"
    token: ${{ secrets.GITHUB_TOKEN }}

Here is what our workflow looks so far:

name: Release a new Version

on:
  push:
    tags:
      - "*"

jobs:
  build:
    if: startsWith(github.ref, 'refs/tags/')
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v1
      
      - name: Setup Node
        uses: actions/setup-node@v1
        with:
          node-version: 12.x
          
      - name: yarn install
        run: yarn install
        
      - name: yarn build
        run:  yarn build:prod
        
      - uses: papeloto/action-zip@v1
        with:
          files: "./dist/webapp/"
          dest: webapp.zip
          
      - name: Push Build to Releases
        uses: ncipollo/release-action@v1
        with:
          artifacts: "webapp.zip"
          token: ${{ secrets.GITHUB_TOKEN }}

Building Scully and Publishing Blog

Next, we are going to add a second job - publishing - that will download our Angular build artifacts from our repos latest release, run Scully build and upload the artifacts to our hosting platform.

First, we will need to listen to the on repository_dispatch as this is how we will trigger our website rebuild when the content on our CMS changes, as explained here. Feel free to use any other GitHub action triggers suitable for your content management system i.e. on push to master on the blog directory if you are using markdown.

on:
  push:
    tags:
      - "*"
      
  repository_dispatch:
    types:
      - publish_blog

Next, we are going to create a publish job, which will run after the build job but if the build job doesn't run, it will run anyway. We will use the if: always() condition to run the job even if the build doesn't. This will run the publish job if a new blog post is published, which will skip the build job, but also when a new release is made, in which case you want the website to be published with changes that were released.

The one downside of this approach is that the publish job will run even if the build job fails.
publish:
  runs-on: ubuntu-latest
  needs: [build]
  if: always()

Next, we will need to setup Node and run yarn install to install NPM packages as Scully needs both to run.

steps:
  - uses: actions/checkout@v1
  - name: Setup Node
    uses: actions/setup-node@v1
    with:
      node-version: 12.x
  - name: yarn install
    run: yarn install

After that, we are going to download our build artifact that we uploaded to GitHub release - webapp.zip - and unzip the content to the dist/webapp directory. To download the artifact from GitHub release, we will be using the dsaltares/fetch-gh-release-asset action.

- uses: dsaltares/fetch-gh-release-asset@master
  with:
    repo: "USERNAME/REPOSITORY"
    version: "latest"
    file: "webapp.zip"
    target: "webapp.zip"
    token: ${{ secrets.GITHUB_PAT }}
NB: For private repository, you will need a GitHub Personal Access Token (PAT) with at least the org:hook scope. Learn more here.

Next, we will create a directory to put the angular webapp build artifacts in and then unzip webapp.zip which we downloaded from GitHub releases.

- name: create dist directory
  run: mkdir -p dist/webapp

- name: Decompress
  uses: TonyBogdanov/[email protected]
  with:
      args: unzip -qq ./webapp.zip -d ./dist/webapp

And finally, run yarn scully for statically site generation of our Angular app:

- name: Run scully
  run: yarn scully

Now we can deploy the Scully build artifact to your website. In this case we will use firebase hosting, which you can do as shown below.

- name: deploy firebase webapp
  uses: w9jds/firebase-action@master
  with:
    args: deploy --only hosting
  env:
    FIREBASE_TOKEN: ${{ secrets.FIREBASE_TOKEN }}

And now our final GitHub Action Workflow looks like this:

name: Publish Blog

on:
  push:
    tags:
      - "*"
 
  repository_dispatch:
    types:
      - publish_blog
      - build_site

jobs:
  build:
    if: startsWith(github.ref, 'refs/tags/')
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v1

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

      - name: yarn install
        run: yarn install
        
      - name: yarn build
        run:  yarn build:prod

      - uses: papeloto/action-zip@v1
        with:
          files: "./dist/webapp/"
          dest: webapp.zip
          
      - name: Push Build to Releases
        uses: ncipollo/release-action@v1
        with:
          artifacts: "webapp.zip"
          token: ${{ secrets.GITHUB_TOKEN }}


  publish:
    runs-on: ubuntu-latest
    needs: [build]
    if: always()

    steps:
      - uses: actions/checkout@v1

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

      - name: yarn install
        run: yarn install
        
      - uses: dsaltares/fetch-gh-release-asset@master
        with:
          repo: "[USERNAME]/[REPO]"
          version: "latest"
          file: "webapp.zip"
          target: "webapp.zip"
          token: ${{ secrets.GITHUB_PAT }}

      - name: create dist directory
        run: mkdir -p dist/webapp

      - name: Decompress
        uses: TonyBogdanov/[email protected]
        with:
            args: unzip -qq ./webapp.zip -d ./dist/webapp
            
      - name: Run scully
        run: yarn scully

      - name: deploy firebase webapp
        uses: w9jds/firebase-action@master
        with:
          args: deploy --only hosting
        env:
          FIREBASE_TOKEN: ${{ secrets.FIREBASE_TOKEN }}

Conclusion

In this article, we have looked at how we can optimize our Scully build time by splitting Angular builds and Scully builds, where we store our Angular builds and re-use the artifacts in future Scully builds.

This may not be necessary for your application if you are not using tools like purge CSS to remove unused CSS, since Angular builds are usually fast for small to medium size applications.

There are few things I skipped like caching NPM dependencies, which can shave off a few more seconds from your build time and I highly recommend you implement following instructions here.

  • Use Webhooks to Trigger GitHub Actions - Link.
  • Getting Started with Scully - Link.
  • Getting Started with GitHub Actions - Link.
  • About GitHub Releases - Link.
  • Angular CDK - Platform Module - Link.