An Introduction to CI/CD for SuiteScript Developers (Part 2)

Written by
Eric Grubaugh
Senior NetSuite Developer
June 19, 2023
min read

Table of Contents

View all guides


Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in eros elementum tristique. Duis cursus, mi quis viverra ornare, eros dolor interdum nulla, ut commodo diam libero vitae erat. Aenean faucibus nibh et justo cursus id rutrum lorem imperdiet. Nunc ut sem vitae risus tristique posuere.

This is some text inside of a div block.

Using SDF and GitHub Actions to automatically deploy to NetSuite

In Part 1 of this series, we discussed the basic definitions and potential benefits of Continuous Integration and Continuous Deployment. In this second part, we create a functioning Continuous Deployment pipeline using the SuiteCloud Development Framework (SDF) and GitHub Actions.

This article is not intended to grant you mastery over all things GitHub Actions; GitHub has already written that documentation for you.

This article is intended to help you establish a simple Continuous Deployment pipeline using GitHub Actions which you can then use to learn and experiment with the possibilities. I strongly urge you to use a test Project, a test Repository, and a Sandbox account while you are learning.

Experience the Ease & Confidence of NetSuite Customizations with Salto

Prerequisites and Resources

This article assumes you have installed, have access to, and have a working knowledge of:

  • git and GitHub: freeCodeCamp has a good introduction.
  • node.js and npm.
  • NetSuite (preferably a Sandbox rather than a Production account). You’ll need a Role that can create Access Tokens and deploy customizations.
  • SDF: There’s a free introductory course over on Salto Leap.
  • a text editor or IDE for writing code.
  • a command-line terminal: The commands you’ll see below were all written for a bash shell within an Ubuntu OS.

Initial Project Setup

We’ll demonstrate our Continuous Deployment pipeline using a brand new git repository and SDF Project. Instrumenting your existing SDF Project(s) will be left as an exercise for you. This initial setup section borrows heavily from GitHub’s Quick Start guide for Actions.

Create an SDF Project

We start by creating a new Account Customization SDF Project and including the Unit Test Framework; this will initialize our SDF Project as an npm package as well. You can name the Project whatever you like. I named my Project and Repository actions-sdf.

cd ~/dev
suitecloud project:create -i

Create a GitHub Repository

Now we need a git repository to connect with our Project, so we log in to our GitHub account and create a new Repository. Once the repository exists, copy its URL:

Add the Project to the Repository

Head back to your terminal and link your SDF Project to your new Repository:

cd ~/dev/actions-sdf
git init
git remote add origin 

Push the initial Project contents to the Repository’s default branch; my default branch is called main:

git add .
git commit -m "Initial project"
git push origin main

Create a new git branch where we’ll add the demo GitHub Actions configuration:

git checkout -b add-actions-config

Create a GitHub Actions Workflow

The top level of organization for GitHub Actions are called Workflows. Workflows are defined as YAML files which live in a Repository under <repoRoot>/.github/workflows/. Create those directories now along with a new YAML file which will hold our demo configuration. I’m naming mine github-actions-demo.yml:

mkdir -p .github/workflows
touch .github/workflows/github-actions-demo.yml

Add the following contents to github-actions-demo.yml (again this content is borrowed straight from GitHub’s Actions Quick Start guide):

name: GitHub Actions Demo
run-name: ${{ }} is testing out GitHub Actions 🚀
on: [push]
    runs-on: ubuntu-latest
      - run: echo "🎉 The job was automatically triggered by a ${{ github.event_name }} event."
      - run: echo "🐧 This job is now running on a ${{ runner.os }} server hosted by GitHub!"
      - run: echo "🔎 The name of your branch is ${{ github.ref }} and your repository is ${{ github.repository }}."
      - name: Check out repository code
        uses: actions/checkout@v3
      - run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner."
      - run: echo "🖥️ The workflow is now ready to test your code on the runner."
      - name: List files in the repository
        run: |
          ls ${{ github.workspace }}
      - run: echo "🍏 This job's status is ${{ job.status }}."

We won’t go through each line of this as most of them explain themselves, but we’ll hit the highlights:

name: GitHub Actions Demo

First, we give our Workflow a readable name GitHub Actions Demo. This is the static name that identifies the Workflow and will be displayed in the list of Workflows under the Actions tab of our Repository on GitHub.

run-name: ${{ }} is testing out GitHub Actions 🚀

Next, we provide a name that will be generated each time our Workflow is run. This example only dynamically displays the user that triggered the Workflow. Alternatively, this property could be used to generate a unique identifier for each run, like a build number or a date/time stamp.

on: [push]

Then we define what Repository events will trigger our Workflow. Here we state that this Workflow should run anytime anyone pushes new code anywhere in our Repository.

In practice, you’ll likely want much more specific conditions and triggers for your Workflow(s). We’ll look at one example of that near the end of the article, but know that there are a ton of options for you.

    runs-on: ubuntu-latest

A Workflow consists of Jobs. Each Job is keyed with an identifier and requires a Runner - a virtual machine operating system to execute within. Our Workflow consists of a single Job identified as Explore-GitHub-Actions which runs in an Ubuntu VM. The Job’s identifier will be displayed in the Workflow visualization on the GitHub UI, so make sure it’s recognizable.

If a Workflow consists of multiple Jobs, each Job will run in its own separate Runner, and all Jobs will run in parallel (unless you define them to do otherwise).

      - run: echo "🎉 The job was automatically triggered by a ${{ github.event_name }} event."
      - name: Check out repository code
        uses: actions/checkout@v3
      - name: List files in the repository
        run: |
          ls ${{ github.workspace }}

The actual activities a Job performs are defined as Steps. Steps can be defined in a variety of ways:

  1. A simple run property which specifies a command to execute.
  2. A multi-line run property which specifies many commands on multiple lines, indicated by the | operator.
  3. Third-parties can package common commands to be dynamically imported and reused, as GitHub has done with the actions/checkout command for checking out code from a repository and cloning it into the Job’s Runner.
  4. Every Step can have a name property which will be displayed after a Job executes.

Our example Job runs a variety of commands and dynamically reads values from various environment variables using the ${{ }} syntax.

Push this new code to your Repository, and the GitHub Actions Demo Workflow will be automatically triggered; you can view its results by navigating to the Actions tab of your Repository, selecting the Workflow in the sidebar, and selecting the current run of the Workflow.

Feel free to experiment and modify this Workflow as much as you wish, exploring the GitHub Actions docs as you do. Once you’re ready to move on, disable the demo Workflow by navigating to the Workflow in the GitHub UI, selecting the ... menu at the top right, and selecting Disable workflow. We don’t need this example running every time a push occurs in the Repository.

Use SDF in an Actions Workflow

Now that we have seen GitHub Actions work, let’s do what we actually came here to do and add SDF and NetSuite into the mix.

Update Dependencies

We’ll start by updating the default package.json file which SDF created for us; primarily we need to add the SDF CLI as a dependency so that the GitHub runner will know to install it. Replace the contents of your Project’s package.json file with the following:

  "name": "actions-sdf",
  "version": "0.1.0",
  "scripts": {
    "test": "jest"
  "devDependencies": {
    "jest": "^29",
    "@types/jest": "^29",
    "@oracle/suitecloud-unit-testing": "^1",
    "@oracle/suitecloud-cli": "^1"

The most significant difference from the default file is the addition of the @oracle/suitecloud-cli package as a devDependency. I’ve additionally named my npm package the same as my Project and Repository, though the name can be whatever you want, and I’ve given it a version number more appropriate to a proof-of-concept Project.

If you’re not familiar with the package.json file, you can read all about its functionality here.

With these updates made, we’re going to make sure the test script correctly invokes the Jest unit test suite which SDF generated during Project creation. In your terminal, from the Project directory:

cd ~/dev/actions-sdf
npm install --acceptsuitecloudsdklicense
npm test

We should then see successful Jest output in our terminal.

Add new Actions Workflow

Now that we know our package is configured correctly locally, let’s get our unit test suite running within GitHub Actions. While you could re-purpose the previous Workflow, I also want to illustrate that a Repository can have any number of Workflows defined in its .github/workflows directory.

Create a new Workflow configuration file in .github/workflows; I’m calling mine sdf-validate.xml.

touch .github/workflows/sdf-validate.yml

We’ll start the Workflow out the same way as the previous configuration, changing the relevant names, and to start we only need to check out the Repository code.

name: SDF Validation
run-name: validate
on: [push]
    runs-on: ubuntu-latest
      - name: Check out repository code
        uses: actions/checkout@v3

The SDF CLI has some operating system dependencies that need to be installed before it will run correctly - namely, node.js and a Java Development Kit (at the time of this writing, it must be Version 17). Luckily, these are extremely common dependencies, so GitHub has already created Actions that will install these for you.

After our checkout Step, we’ll add the current “Long-Term Support” version of node.js using the setup-node Action:

- name: Install node
  uses: actions/setup-node@v3
    node-version: 'lts/*'

Following that, we’ll do the same for JDK version 17 using the setup-java Action:

- name: Install JDK
  uses: actions/setup-java@v3
    java-version: '17'
    distribution: 'oracle'

Run Unit Tests with GitHub Actions

With these dependencies in place, we can replicate the installation and test commands we ran locally in our Workflow by adding them as Steps:

- name: Install package
  run: npm ci --acceptsuitecloudsdklicense
- name: Run unit tests
  run: npm test

There is a subtle but important difference here in that we are using npm ci instead of npm install to perform the package installation. You can read about the details of the ci command yourself, but the primary significance of the ci command is that it’s designed for use in a Continuous Integration environment (like GitHub Actions) and performs a clean install each execution.

Push this new code to your Repository, then navigate your way through the Actions tab to see the results. You should see outputs similar to the following under each Step in the sdf-validate Job:

To see what happens when your unit tests fail, edit the __tests__/sample-test.js like so:

it('should assert strings are equal', () => {
  const a = 'foobar';
  const b = 'foobar';
  // comment this line:
  // expect(a).toMatch(b);
  // and add this line:

Push this change, and your unit test should fail, thus causing the Workflow to exit with a failure status:

Now restore the test to its working state, and we’ll move on to validating and deploying our Project.

Authenticate to NetSuite from GitHub Actions

In order to interact with NetSuite via SDF, your Workflow will need a valid NetSuite access token. Begin by creating a new token with an appropriate Role in your target account(s); the native “Developer” role is a fine default if you don’t have a more specific one.

Next, we need a safe place to store that token information.

IMPORTANT: Do not hardcode your token values in your config file. Do not store your token details in plaintext in your git repository. Doing so would compromise your token and thus your NetSuite account.

GitHub Actions provides us with the Secrets mechanism for storing sensitive data like token values and passwords. In order to store our token information as Secrets, we first need to create an Environment.

Navigate to your Repository > Settings > Environments. Once there, create a new Environment that will represent your target NetSuite account. You may name it whatever you like; I created one named production to represent my Production account and one named sandbox to represent my demo account.

Within your new Environment, create a new Environment Variable to store your target account’s ID. I named my variable NS_ACCOUNT_ID. Next, add two new Environment Secrets to store your token information. I named my secrets NS_TOKEN_ID and NS_TOKEN_SECRET to store the Token ID and Token Secret values, respectively. These variables will let us reference our values programmatically within our Workflow configuration rather than hardcoding the values.

It’s a good idea to prefix environment variables/secrets with an identifier for the corresponding system (e.g. NS_ for NetSuite) as it’s fairly likely your GitHub Actions might communicate with and store information for several systems (e.g. Slack, Jira, NetSuite, etc).

Repeat the Token and Environment creation steps for any additional target NetSuite accounts you might want to use.

Now we should be able to authenticate to NetSuite within our Workflow. Back in the sdf-validate Job, we need to tell the Job which Environment to use; we do so by adding an environment property to the Job definition:

    runs-on: ubuntu-latest
    environment: production
    steps: ...

You can specify the name of any of the Environments you just created; once again, I strongly recommend using a Sandbox account while you are learning and testing.

With an Environment assigned, the Job can now access the Environment’s Variables via the vars context and its Secrets via the secrets context.

Add a new Step for authentication using the account:savetoken SDF command.

- name: Authenticate project against ${{ vars.NS_ACCOUNT_ID }}
  run: ./node_modules/.bin/suitecloud account:savetoken --account ${{ vars.NS_ACCOUNT_ID }} --authid ${{ vars.NS_ACCOUNT_ID }} --tokenid ${{ secrets.NS_TOKEN_ID }} --tokensecret ${{ secrets.NS_TOKEN_SECRET }}

Note that I have to add the fully qualified path to the suitecloud command; this is because the node_modules/.bin/ directory is not on our PATH by default. To avoid this, you could add a Step in the Job to do so, but I leave that as an exercise for you.

Push this change, then monitor the Job’s output to see if authentication is working.

Validate the SDF Project with GitHub Actions

Once your Workflow can authenticate to NetSuite, it can start to leverage SDF to do some work! One thing we nearly always want to do before we deploy an SDF Project is validate it. Personally, I like to validate using the project:deploy command with its dryrun option (if you want to learn why I prefer that, check out the aforementioned SDF course on Salto Leap).

Instead of just adding a Step to our Job which invokes SDF directly, though, I’m going to make a validate script within our npm package definition, and the Job will invoke that script instead. The main reason I do this is so I can run the exact same command both from my local machine and within GitHub Actions; this helps to ensure consistency and avoid any gaps between the two environments.

In package.json add a new validate property to the scripts Object:

"scripts": {
  "test": "jest",
  "validate": "suitecloud project:deploy --dryrun"

Then in your Workflow, add a new Step to invoke this validate script using the npm run command:

- name: Validate project
  run: npm run validate

A secondary benefit of using npm scripts to run suitecloud operations is that I no longer have to specify the full path to the command like I did with the authentication step; npm knows about the node_modules/.bin/ path by default, whereas GitHub Actions does not.

As usual, push these additions to the repository and observe the validation of your SDF Project within your Actions Workflow.

If validation were to fail, the Job would exit without proceeding on to deployment (once we add that step … right now).

Deploy the SDF Project with GitHub Actions

Within our Project successfully validating, it’s finally time to deploy it to NetSuite. This will unfold in exactly the same way as validation.

First we add a deploy script to our npm package:

"scripts": {
  "deploy": "suitecloud project:deploy"
  "test": "jest",
  "validate": "suitecloud project:deploy --dryrun"

Then we add a Step to our Job to invoke the new script:

- name: Deploy project
  run: npm run deploy

Once we push these changes, we should see the Workflow successfully deploy our SDF Project.

Since we’re so far testing this on an empty SDF Project (no source files or Object definitions), perhaps the actual results of validation and deployment aren’t that interesting, but we can clearly see the SDF commands being invoked, contacting our NetSuite account, and returning appropriate results.

Set up Branch Protections

Before you start adding in a few files and Objects of your own to make the results more interesting and realistic (which you totally should do, just not yet), remember that this Workflow is executed whenever anyone pushes any changes on any branch in our Repository. In practice, that is very likely not what you want, especially for a Production environment.

Let’s ensure that only pushes on the main branch will trigger a deployment to our Environment.

First, we establish a Branch Protection Rule on our Environment. While in practice you would likely do this only for your Production Environment, for testing and learning purposes, you can set this up on your Sandbox Environment as well.

In your Environment definition, add a new Deployment Branch, and specify the default branch in your Repository; again mine is named main:

From now on, any Jobs that target this Environment will immediately fail unless they’re triggered from the main branch.

Additionally, we can restrict our entire Workflow so that it only responds to push events on the main branch. Back in the Workflow configuration, we want to modify the on property near the top like so:

    branches: [main]

When you push this change to your repository, assuming you are still operating on a branch other than main, observe that the Workflow does not get triggered. Only push events on the main branch will trigger our Workflow (and thus a deployment).

What can you build now?

At this point, you have a fully functioning Continuous Deployment pipeline using SDF within GitHub Actions. It runs your unit tests, validates your SDF Project, and if all goes well, deploys the Project to NetSuite.

You can find all of my example code over in my GitHub Repository.

Next, you might check out GitHub’s Starter Workflows for basic configuration examples, and you might read up on npm scripts and lifecycles to see what else you might be able to accomplish with those.

Some ideas for what else your Workflow could do for you:

  • Automatically send a Slack notification when a deployment succeeds/fails
  • Automatically merge a feature branch into main if all your unit tests pass
  • Run ESLint to automatically check and fix your code and formatting according to your style rules
  • Run JSDoc to generate HTML documentation from your code and publish it to a docs site

I’ve introduced the major building blocks of GitHub Actions in Workflows, Jobs, and Environments, and I’ve shown how to use SDF in that construction, and I’ll be back in Part 3 of the series to ramp up the complexity of our Workflow. For now, it’s your turn to build something great with those blocks!

Written by
Eric Grubaugh

Since 2012, Eric has been designing and developing SuiteScript solutions, coaching NetSuite developers on doing the same, and advising SuiteScript teams on building healthy, effective practices. He also launched the SuiteScript Stories podcast.

Written by