Maina Wycliffe
Posted on April 19, 2021
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 theorg: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/zip@1.0
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/zip@1.0
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.
Links
Posted on April 19, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.