How to Properly Set Up Continuous Deployment to Netlify

Introduction

In my previous post on Hexo workflow automation, I mentioned one of the reasons for migrating from GitHub Pages to Netlify is that both are free. Actually, that's not entirely accurate. Netlify provides 300 free credits per month, but a single deployment costs 15 credits. If I kept the same deployment strategy used in GitHub Actions, these credits would be depleted quickly, making my blog inaccessible (visitor requests and egress traffic also consume credits, costing relatively less though). Therefore, setting up a proper continuous deployment strategy is crucial. In this post, I'll explain the different deployment types on Netlify and provide a more sensible GitHub Actions deployment strategy.

Netlify Deployment Types

Netlify offers three deployment types: Production deploys, Deploy Previews, and Branch deploys.

The Production type is for official releases, while the latter two are primarily for experimenting. All three types generate a user-accessible site. The key difference is that Production deploys cost 15 credits each, while the other two are free. Given that the latter two are quite similar, this post will focus on Deploy Previews.

While we could technically use only Deploy Previews as our production site — benefiting from completely free deployments — this goes against standard release practices and doesn't facilitate sound decision-making. The correct release workflow should be: automatically deploy to Deploy Previews, and then manually trigger a Production deploy once the site is confirmed to be stable.

GitHub Actions

Based on the strategy discussed above, I use the master branch as the development branch. Every push to master triggers an automatic deployment to Deploy Previews. The prod branch serves as the production release branch; manually merging master into prod triggers a Production deploy.

The Netlify CLI supports different deployment types via the --prod flag. Using this flag triggers a Production deploy; omitting it creates a Deploy Preview.

Sites for different deployment types naturally have different URLs, controlled by the --alias parameter. Assuming our production site URL is blog.netlify.app, setting --alias=preview adds a preview-- prefix, resulting in preview--blog.netlify.app. While we could set this parameter to different values (e.g., using the SHORT_SHA variable shown below), Netlify doesn't seem to impose a hard limit on the number of unique URLs. However, I find using different aliases unnecessary and hard to manage, so I consistently use the same prefix.

The final deployment code looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- name: Deploy
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
run: |
SHORT_SHA=${GITHUB_SHA::7}

if [[ "${{ github.ref_name }}" == "prod" ]]; then
npx netlify deploy \
--prod \
--no-build \
--dir=./dist \
--message="prod deploy ${SHORT_SHA}"
else
npx netlify deploy \
--no-build \
--dir=./dist \
--alias=preview \
--message="preview deploy ${SHORT_SHA}"
fi

Some Reflections

From a purely cost perspective, GitHub Pages remains one of the few truly free static site hosting solutions. If we build artifacts locally and upload them to GitHub Pages, we get unlimited deployments. Even using GitHub Actions, which offers 2000 free minutes per month, a typical build completes in under 3 minutes, allowing for far more production releases than Netlify's credit system permits.

Looking back, migrating my blog just because I initially thought Netlify was also completely free seems like a decision that perhaps lacked sufficient consideration.

References