Deployment with GitHub Actions for Nuxt projects
We explored how to create a Git-based headless CMS with Nuxt, Nuxt Content module, and Git submodules in my previous post. To enable auto deployment when there is new content and complete the workflow with a submodule, we need extra help from an automation tool, such as GitHub Actions.
TL;DR
You can use the tutorial as a reference to set up any project using Git submodules, without limit to Nuxt. All you need is to perform the following:
- Create a GitHub PAT token to allow GitHub Actions to access the desired repositories and dispatch events or hooks.
- Create an Actions workflow file for the submodule repo to dispatch an
respository_dispatch
event to the main repo whenever content changes. - Create an Actions workflow file for the main repo to trigger whenever it receives an
respository_dispatch
event. The workflow updates the local version of the submodule repo in the main repo, and deploy the main repo to Vercel or Netlify using Vercel Deploy Action (or Netlify Deploy Action). - Sit back and enjoy the automation. 🙂
Need more details? Let's go.
GitHub Actions
GitHub Actions, developed by GitHub, are scripts containing YAML code. These scripts help automate development workflow by a series of custom tasks (actions) directly on GitHub repositories. The action codes live as part of the repository, allowing people to modify them in the local environment and commit them accordingly.
You can manually create your actions script or choose and combine different existing actions on GitHub Actions Marketplace to make one. An example of searching for Vercel Deployment Action in the Marketplace for GitHub Actions is as below:
GitHub Actions can run on any platforms such as Linux, Ubuntu, etc., and any frameworks, including Node.js, Rust, Python, etc. Each GitHub Action provides a detailed log, which is useful for deployments' troubleshooting on run-time.
With GitHub Actions, we can afford to have testings enabled for our codebase in multiple environments simultaneously. A usual use case for GitHub Action is an E2E (end-to-end) test suite that runs whenever there is a new PR created. It ensures code coverage and lessens the burden for code reviewers.
So, how do we run GitHub action(s)?
Triggering a GitHub Action
We can trigger a GitHub Action using the following types of events:
- Workflow
- Scheduled
- Webhooks
- External
Great. How do we start working with GitHub Actions?
Well, first, we need to create a workflow .yml
file.
Create your action script
To start using GitHub Actions, go to your GitHub Repository and select Actions tab.
We can ignore the suggested templates and choose to set up a workflow ourselves as following:
Once clicked, GitHub will automatically create a new .yml
file located in .github/workflows/ directory
with some sample code and redirect us to an online editor session to start modifying the file.
We will write the workflow code in YAML language, which is strictly sensitive and in order. Below are a few common basic rules to note for an easy start with YAML syntax:
- A list or object's property starts with
-
(a dash and space)fruits: - orange - kiwi # it equals to fruits: [orange, kiwi] or fruits: {orange, kiwi}
- A field with value is denoted by
key: value
(there must be a space between colon and value).name: Maya
- We can span multiple lines for values using
|
- result will be the same as an appearance in code (with trailing spaces). And>
will convert new lines to spaces, making all into one single line.# the following will appear the same description: | I'm a developer and a speaker #here it will appear as Senior Frontend in one single line title: > Senior Frontend
- Item's nested property, or step's breakdown for a sequence need to have single space as line prefix, in respect to its nesting parent. Otherwise, it will be treated at the same level as the parent instead of nesting inside.
steps: step 1: # nested step actions step 2: # nested step actions name: Deploy
This means there are two stepsstep 1
,step 2
nested insidesteps
, andname
andsteps
are at the same level. - Use
#
for commenting.
Depending on what you want to achieve, you can edit the created .yaml
file to add/modify steps or actions. Indeed, you can always choose an existing template for your workflow and change it per need, instead of creating from scratch 😉.
Next, let's design our workflow and start coding!
Design your workflow
We are going to set up a workflow that connects two repositories:
portfolio
- this is the main public repository where the codebase of the website resides. It has a submodule which iscontent
- the private submodule repository that contains all the content, blog posts, etc.
Our workflow goal is straightforward: whenever there is a commit merged to our submodule repository content
, it will trigger the build and deployment on our main repository - portfolio
The workflow diagram is as follows:
Based on the above design, we need to perform the following tasks:
- Once a commit is merged to the
master
branch ofcontent
, we need to make sure it's a valid commit and then dispatch an event to inform theportfolio
repo about this change. - Upon receiving this event, the repo
portfolio
performs agit submodule update --remote --recursive
to have its submodules'refs
updated. The local content version at this point reflects the latest changes. - Then it needs to push a commit with the updated
refs
to themaster
branch to keep itself synchronized withcontent
. - Finally, it triggers a new build and deploys the latest version to production, using the relevant CLI command (
vercel —prod
for Vercel ornetlify deploy
for Netlify).
Great. Before we continue, there is one thing we need to do. Communicating between two repositories or more requires authentication. Thus, to ensure the workflow between two repositories happens securely, we need to setup GitHub PAT - personal access token.
Setting up the PAT and the secrets
PAT - Personal Access Token is required for authentication when using GitHub API in place of passwords. Each PAT is tied to a set of access permissions options we selected during the creation.
And dispatching "communicating" events to a repository needs a unique PAT with either:
write
access to the target repository.- Or access to
public_repo
is enough if the target repository is public.
In our workflow, we only need to enable public_repo
permission, since our target portfolio
repository is a public one.
To generate a new PAT, navigate to your GitHub Settings, and select Developer Settings from the left side menu.
Select Personal Access Token on the left side menu, then hit "Generate New Token" button to create a new PAT.
Finally fill in the note to describe what the PAT is for, and tick public_repo
to enable the access.
Once done, hit the button "Generate token" at the bottom of the page. And our PAT is ready for us to copy and use.
Great. We have to add it as an encrypted environment variable and use it for GitHub Actions in both repositories.
Adding secrets
key
We can add the newly created PAT as a secret key in the following steps:
- Navigate to the
content
repository Settings tab and select Secrets on the left side menu. - Create a new repository secret.
- Paste the key you copied to the Value section, and give it the name
DISPATCH_HOOK_TOKEN
. Then hit "Add secret" to save the new key.
- Repeat the same steps to
portfolio
repository and haveDISPATCH_HOOK_TOKEN
key created with the same value.
This variable is available to access workflow running environment using secrets
. It is a global object given by GitHub, which contains all the encrypted environmental variables to use securely in the workflow. To access the created key, we use secrets.DISPATCH_HOOK_TOKEN
.
So far, so good. Now, let's put our workflow into code, shall we?
Setting the workflow for submodule
Let’s create a workflow file main.yml
followed the instruction above in the submodule repo content
. Then we remove the sample code provided in main.yml
and start with only the following:
In which:
on
section defines when the action will run.jobs
section contains a list of tasks to run for a workflow, in parallel or sequentially.runs-on: ubuntu-latest
means we want to run our workflow on Ubuntu environment.steps
defines the sequences of tasks within a job to be executed.uses
defines what GitHub Action to use for a task. Hereactions/checkout@v2
is the action for checking out a repo for the workflow to access it during run time. This task is required for the workflow to perform tasks on the repo in its local environment.
Tip: Do not worry if you get lost. GitHub editor has auto hint and error highlighting to help you get started smoothly. You can also refer to YAML Syntax Documentation for better understanding.
Great. The workflow file is ready for the next modification - defining how to communicate with our target repo - portfolio
from the content
.
Dispatch event to communicate between repositories
For communication between repositories, GitHub API provides us a Webhook event called respository_dispatch
. This event allows us to trigger a specific workflow outside of GitHub according to a custom event_type
value sent in the Webhook payload. We can set the target event_type
in payload when we create the dispatch event.
To enable this Webhook event, we perform the following:
- Use the Repository Dispatch Action and click the "Use the latest version" button.
- Hit the Copy icon to copy the code snippet.
- Paste in our sample code as the new step under
steps
- Provide the configurations for the action:
with: token: ${{ secrets.DISPATCH_HOOK_TOKEN }} repository: ${{target_repo}} event-type: merge-content client-payload: '{"message": "New blog posted!"}'
In which:token
- the default GitHub token doesn't have the scope for dispatchingrespository_dispatch
event. For this purpose, we use ourDISPATCH_HOOK_TOKEN
created previously by usingsecrets.DISPATCH_HOOK_TOKEN
.repository
- the target repo to receive this event. Replace${{target_repo}}
with your target repo path in the formatyour-github-user/repo-name
event-type
- what type of event you want to tell the other repo. Type should be unique as on the target repo, GitHub will trigger a specific workflow according to an event type it attaches. In our example we usemerge-content
client-payload
- anything else you would like to add, it can be a Twitter message for posting after the target repo finishes deploying.
Our code becomes:# main.yml name: CI on: push: branches: [ master ] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 # Dispatch an event to the consumer repo - name: Repository Dispatch uses: peter-evans/repository-dispatch@v1.1.1 with: token: ${{ secrets.DISPATCH_HOOK_TOKEN }} repository: ${{target_repo}} event-type: merge-content client-payload: '{"message": "New blog posted!"}'
- We only want to dispatch this event on pushed commits related to content (and not a merge or any changes related to our workflow files). For that, we define the
paths-ignore
, the target type of request to run the request, and replace the original section underon
as below:# Controls when the action will run. # Triggers the workflow on push or pull request # events but only for the master branch on: push: branches: [ master ] paths-ignore: - '.github/workflows/**'
That's it. Our workflow for content
submodule is complete with the following code:
```yaml
# main.yaml
name: CI
on:
push:
branches: [ master ]
paths-ignore:
- '.github/workflows/**'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
# Dispatch an event to the consumer repo
- name: Repository Dispatch
uses: peter-evans/repository-dispatch@v1.1.1
with:
token: ${{ secrets.DISPATCH_HOOK_TOKEN }}
repository: ${{target_repo}} # replace this with your repo
event-type: merge-content
client-payload: '{"message": "New blog posted!"}'
Next, we define the workflow to trigger for the target repository - portfolio
whenever merge-content
event type is dispatched from content
.
Setting up the deployment workflow for the main repo
On the main repo - portfolio
, let's add a new workflow file to handle the dispatched event merge-content
from its submodule using the same instructions.
To make it more specific, we change the default filename from main.yml
to deploy_content.yml
, and change the name
key to a more self-explained title
name: Deploy on new content dispatch
We need to configure when the action runs. In our case, it happens when it's a repository_dispatch
event whose type is merge-content
.
name: Deploy on new content dispatch
on:
repository_dispatch:
types: [merge-content]
For this workflow, we have the following steps:
- Check out the submodule repo, so our job can access it for the build.
- Update the submodule ref to reflect the new content
- Make a commit on this updated ref and push back to the repo, so the repo is always up-to-date.
- Build and deploy.
Let's get started with each step, shall we?
Check out the submodule repo into the main repo
This step is essential to have all the submodule content available for building in the main repo. To check out a specific repo as a submodule, we need to use GitHub Action actions/checkout@v2
with the following configurations:
token
- the same PAT token we put to allow dispatching events oncontent
repo -DISPATCH_HOOK_TOKEN
submodules
- we set this flag totrue
to inform this cloning task is applied for submodule, not for a regular repo
And the code is:
- name: Clone/Checkout submodules
uses: actions/checkout@v2
with:
token: ${{ secrets.DISPATCH_HOOK_TOKEN }}
submodules: true
Cool. Since the repo is checked out, we can update the submodule local version's refs to reflect the new content.
Update the submodule
This step requires two separate sub-steps to happen sequentially:
- Update the submodule's
refs
from the remote repo by the following step:- name: Update submodules content run: git submodule update --remote --recursive
- Pull the remote repo's content according to the latest
refs
- name: Pull the content run: git pull
The submodule's content in the main repo should be up-to-date by this point.
Next, we need to update the main repo with the new submodule refs
received.
Commit the new submodule's refs
For this step, we use Git Auto Commit Action, which can be found on the marketplace:
And add the following code:
- name: Update submodules index
uses: stefanzweifel/git-auto-commit-action@v4
With the following configurations for the commit:
with:
# Required
commit_message: Update submodule ref
# Optional commit user and author settings
commit_user_name: My GitHub Actions Bot
commit_user_email: actions@github.com
commit_author: My GitHub Actions Bot <actions@github.com>
In which:
commit_message
- Description of the commit. This field is required.commit_user_name
- Name of the committer.commit_user_email
- Email of the committer. In our example, we use the Actions Botactions@github.com
commit_author
- The author of the commit.
Our code will look like:
name: Deploy on new content dispatch
on:
repository_dispatch:
types: [merge-content]
jobs:
mergeContent:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Clone submodules
uses: actions/checkout@v2
with:
token: ${{ secrets.DISPATCH_HOOK_TOKEN }}
submodules: true
- name: Update submodules content
run: git submodule update --remote --recursive
- name: Pull the content
run: git pull
- name: Update submodules index
uses: stefanzweifel/git-auto-commit-action@v4
with:
# Required
commit_message: Update submodule
# Optional commit user and author settings
commit_user_name: My GitHub Actions Bot
commit_user_email: actions@github.com # to change
commit_author: My GitHub Actions Bot <actions@github.com>
Awesome. Finally, let's deploy the project to production!
Build and deploy with Vercel
One advantage in using Vercel is that it auto-detects the build command and directory to deploy based on your main framework. Hence we do not need to specify a job step for building the code and dependencies.
Firstly, we need to link our project to Vercel. To do so, you should set up Vercel CLI on your local machine and run the following command in the project root directory.
vercel
This step is required for Vercel CLI to know which project scope you want to deploy to. Once you finish the setting up process, a .vercel
directory will be added to your directory with a project.json
file, containing the following:
/*project.json*/
{
"orgId": "example_org_id",
"projectId": "example_project_id"
}
Copy these keys and save them as VERCEL_ORG_ID
and VERCEL_PROJECT_ID
in your GitHub Secrets.
To deploy to Vercel, we will use Vercel Action:
And insert the following code to our workflow file:
name: Deploy to Vercel
uses: amondnet/vercel-action@v19.0.1+4
with:
vercel-token: ${{ secrets.PAT_VERCEL_TOKEN }} # Required
vercel-args: '--prod' #Optional
vercel-org-id: ${{ secrets.VERCEL_ORG_ID}} #Required
vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID}} #Required
In which:
vercel-token
- A required token to deploy to Vercel. You can generate the token in your Vercel dashboard by going to User Settings > Tokens and clicking on Create:
And after setting the name of the token, click the Create token button to have it generated:
Once created, copy the new token by hitting the Copy icon.
Then create the new secret keyPAT_VERCEL_TOKEN
in the repository's settings with the generated token's value.vercel-args
- Extra arguments to pass for deployment. We use--prod
to indicate the deployment is for production only.vercel-org-id
- it is theorgId
field, which can be found in the local.vercel
file. We need this field to link the project to Vercel.vercel-project-id
- it is theprojectId
field, which can be found in the local.vercel
file. We need this field to link the project to Vercel.
Hit the Commit
button, and our workflow is ready.
Bonus: Build and deploy with Netlify CLI
If you wish to deploy using Netlify, Netlify Deploy Action is available for us to use.
Insert the following code as a new job step to our deploy-content.yml
file.
- name: Netlify Deploy
uses: jsmrcaga/action-netlify-deploy@v1.2.0
with:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
NETLIFY_DEPLOY_TO_PROD: true
build_command: yarn generate
build_directory: dist
install_command: yarn
In which:
NETLIFY_AUTH_TOKEN
- authentication token to deploy your site on Netlify. You can generate one by navigating to your Nelify's account Settings > Applications > Personal access Tokens.
You can create a new token by clicking on "New access token". After you give the new token a meaningful name, generate and copy it.
Finally, create a new secret keyNETLIFY_AUTH_TOKEN
in the repository settings with the copied token's value.NETLIFY_SITE_ID
- the site where you deploy your site to. You can find it through the Site Settings > General > Site details > Site information > API ID.
<img src="https://res.cloudinary.com/mayashavin/image/upload/f_auto,q_auto/v1605100057/articles/github/APP_ID_Netlify loading="lazy" alt="Getting Netlify Site ID" class="mx-auto mt-2"/>
Once you copied it, create a new secret keyNETLIFY_SITE_ID
in the repository settings with that value.NETLIFY_DEPLOY_TO_PROD
- whether we want to deploy to production directly. We set it totrue
to enable deployment to production automatically.build_command
- which CLI command we use to build our project. Unlike Vercel, Netlify doesn't detect the build command automatically from your main framework, but leaving the freedom to users. We need to specifyyarn generate
since we want to build a Nuxt static site.build_directory
- where the generated code locates for Netlify to deploy. For Nuxt project, it isdist
by default.install_command
- whether we useyarn
ornpm
to install dependencies. In our case we useyarn
.
That's it. Hit the Commit
button to save the file to the repository, and our workflow is ready to go.
Run the workflow
Let's add an example.md
file in content
repo and commit it to the master
branch.
---
title: Example
---
This is an example file for content.
More example
## Heading 2
Once the commit is pushed, we can go to the Actions tab of both repositories and select the related workflow to view the triggering status:
- In
content
repo's workflow - In
portfolio
repo's workflow
Once all the jobs are green, we complete our deployment workflow! Whenever we make new content, commit and push it to the master
branch of content
. The new content will then be deployed to production automatically! 🎉
Resources
- Nuxt-blog-template - template repository for setting Nuxt Content project with submodule
- Content-submodule-template - template repository for content management that can be used as submodule.
Summary
In general, automation is essential. By setting up automation for complicated and time-consuming routine tasks like CI/CD (continuous integration/ continuous deployment) on complicated infrastructure, we can afford to be more productive and focus on a more significant area, such as code quality. By setting up your Nuxt project with Nuxt content and Git Submodule as Headless CMS, having a seamless automatic deployment workflow is essential. Once you have the workflows set up, you are relaxed and rest assured that you don't have to manually check and deploy whenever there is a content change from now on. Automation FTW!
Also, guess what, you just learned a new DevOps skill 😉. Isn't it awesome?
After trying both Vercel and Netlify, I personally prefer the setting up for Netlify deployment using GitHub Actions. The user experience in Netlify is more trivial and seamless, with less local setup required for the CLI tool. At the same time, you have more control over how you want to build your projects (which command to give). And Netlify provides such an out-of-the-box solution in working with submodules!
What's else? How about trying to automate your unit testing workflow with GitHub Actions and ensure you cover your codebase with tests 😄? I'm curious to see what you can create with GitHub Actions to ease your development cycle.
👉 If you'd like to catch up with me sometimes, follow me on Twitter | Facebook.
Like this post or find it useful? Hit the share button 👇🏼 or Buy me a coffee 😉