If you're building a Google Workspace Add-on, the runtime lives somewhere weird. Apps Script is a server you can't SSH into and a UI you can't scp files to — every change has to go through Google's editor or their API. The bridge between your repo and that environment is clasp (Command Line Apps Script), and once you wrap it in GitHub Actions, deploying becomes a normal git push.
This post walks through the whole pipeline end-to-end: installing clasp, setting up the config that links your repo to a specific Apps Script project, and finally a GitHub Actions workflow that pushes every commit to main straight into Apps Script.
What clasp Is
clasp is a small Node CLI from Google that talks to the Apps Script API. The mental model is simple:
- Your Apps Script project lives at a URL like
script.google.com/.../editand is identified by ascriptId. clasp pushuploads the.gs,.html, andappsscript.jsonfiles in a local directory into that script project, replacing the server-side copy.clasp pulldoes the reverse — handy if someone edited code in the Apps Script web editor and you want to bring those changes back to git.clasp deploycuts a versioned, named deployment — the one the add-on store and your end users actually run. Without it,clasp pushonly updates the@HEADversion that nobody but the developer sees.
That's the whole tool. No build system, no opinions about your code — just a sync mechanism between a folder on disk and a script project in Google's cloud.
Step 1 — Install clasp
clasp is a global Node package. Make sure you have Node 20+ installed, then:
npm install -g @google/clasp
clasp --version
Before clasp can talk to your account, you need to enable the Apps Script API for your Google user. Go to script.google.com/home/usersettings and toggle Google Apps Script API to On. This is a one-time switch per user account — easy to miss, and clasp errors out cryptically if you skip it.
Now log in:
clasp login
A browser window opens and walks you through OAuth. When it's done, clasp drops your credentials into ~/.clasprc.json. We'll come back to that file when we set up CI.
Step 2 — Get the scriptId
Every Apps Script project has a unique scriptId — that's how clasp knows which project to push to. There are two ways to get one.
If the project doesn't exist yet, create it from the command line:
mkdir my-addon && cd my-addon
clasp create --type standalone --title "My Add-on"
--type can be standalone, sheets, docs, slides, forms, or webapp. Pick what matches your add-on. clasp creates the project on Google's side and writes a .clasp.json for you with the new scriptId already filled in.
If the project already exists (you created it in the Apps Script editor, or someone shared one with you), open it at script.google.com and look at the URL:
https://script.google.com/d/1AbCdEfGhIjKlMnOpQrStUvWxYz0123456789AbCdEfGhIjKlMnOp/edit
└────────────────────── scriptId ──────────────────────┘
The long alphanumeric segment between /d/ and /edit is your scriptId. You can also find it inside the Apps Script editor under Project Settings → IDs.
Step 3 — Set Up .clasp.json
.clasp.json is the file that pins your local directory to a specific Apps Script project. It lives at the root of your repo:
// .clasp.json
{
"scriptId": "1AbCdEfGhIjKlMnOpQrStUvWxYz0123456789AbCdEfGhIjKlMnOp",
"rootDir": "./src"
}
Two fields, both important:
scriptId— the project you want clasp to push to. Paste the one you got from Step 2.rootDir— the local folder clasp should treat as the project source. Everything inside this folder gets uploaded; everything outside is ignored. If you omitrootDir, clasp uses the current directory, which usually means it tries to upload yournode_modulesand.gitfolders. Always set it explicitly.
A couple of optional fields you may run into:
scriptExtensionsandhtmlExtensionslet you customize which file extensions clasp treats as server code or HTML.filePushOrderforces specific files to be pushed before others — only relevant if you have load-order dependencies between.gsfiles.
For most projects, the two-field version above is all you need.
Step 4 — Lay Out the Files
clasp doesn't care how your files got into rootDir. As long as the folder contains valid Apps Script files, it'll push them. A minimal layout looks like this:
my-addon/
.clasp.json
.claspignore // optional — files to skip on push
src/
appsscript.json // manifest: scopes, runtime, add-on config
Code.gs // your server-side functions
Sidebar.html // any HTML you serve via HtmlService
Three things to know about the file types:
.gsfiles are Apps Script server code. clasp converts them transparently to/from the editor's native format..htmlfiles are templates served viaHtmlService.createHtmlOutputFromFile().appsscript.jsonis the project manifest. It declares OAuth scopes, the runtime version (V8), time zone, and — for add-ons — the menu entries and triggers users see. clasp will refuse to push without one.
If you have files inside rootDir that shouldn't be uploaded (build scripts, READMEs, anything that isn't .gs/.html/appsscript.json), add a .claspignore next to .clasp.json:
**/**
!appsscript.json
!**/*.gs
!**/*.html
That pattern says "ignore everything, then un-ignore the file types Apps Script actually understands."
Once the layout is in place, you can deploy locally with two commands:
clasp push -f
clasp deploy --description "first version"
-f skips the interactive "manifest changed" confirmation — fine to leave on, and required for CI later.
Step 5 — Authenticate clasp in CI
Local deploys work because clasp login left credentials in ~/.clasprc.json. GitHub Actions runners don't have that file, and they can't run a browser flow, so we hand them the credentials directly via a secret.
The file looks roughly like this:
{
"token": {
"access_token": "ya29....",
"refresh_token": "1//0g....",
"scope": "https://www.googleapis.com/auth/script.projects ...",
"token_type": "Bearer",
"expiry_date": 1736000000000
},
"oauth2ClientSettings": {
"clientId": "xxxxxxxxxxxx.apps.googleusercontent.com",
"clientSecret": "GOCSPX-xxxxxxxxxxxx",
"redirectUri": "http://localhost"
},
"isLocalCreds": false
}
The refresh_token is the load-bearing piece — as long as nobody revokes the grant, clasp can use it to mint fresh access tokens for months.
To stash it in GitHub:
- On a workstation, run
clasp login(skip if you already did Step 1). cat ~/.clasprc.jsonand copy the entire output.- In your repo on GitHub: Settings → Secrets and variables → Actions → New repository secret.
- Name:
CLASPRC_JSON. Value: the full JSON you just copied.
💡 Tip: Generate this token from a dedicated Google account that owns the Apps Script project — not your personal account. If a developer leaves the team, you don't have to rotate anything.
Step 6 — The GitHub Actions Workflow
Now the payoff. Drop this into .github/workflows/deploy.yml:
name: Deploy Apps Script
on:
push:
branches: [main]
workflow_dispatch:
jobs:
deploy:
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Install clasp
run: npm i -g @google/clasp
- name: Restore clasp credentials
run: echo '${{ secrets.CLASPRC_JSON }}' > ~/.clasprc.json
- name: Push to Apps Script
run: clasp push -f
- name: Create a versioned deployment
run: |
DESC="ci-${GITHUB_SHA::7}-$(date -u +%Y%m%d%H%M)"
clasp deploy --description "$DESC"
What each piece is doing:
on: push: branches: [main]— every merge tomaintriggers a deploy.workflow_dispatchadds a "Run workflow" button in the Actions tab for manual re-runs.environment: production— opt-in to a GitHub Environment. If you configure required reviewers on it, every deploy waits on a human approval before proceeding. Worth doing for any add-on with real users.- Restore clasp credentials — writes the secret back into
~/.clasprc.jsonso clasp finds it where it expects. Don'tcatorechothe secret anywhere else; GitHub masks it automatically only when used through${{ secrets.* }}. clasp push -f— uploads everything inrootDirto the Apps Script project. The-fis mandatory in CI; without it, the "manifest changed" prompt hangs the runner.clasp deploy --description— cuts a new versioned deployment. The description includes the short SHA and a timestamp so you can trace any deployment back to the exact commit. This is the step most people forget — without it,clasp pushonly updates@HEAD, which your installed users never see.
Commit the workflow, push, and watch the green check. From here on, deploying is just git merge.
What We Don't Do (and Why)
We don't commit ~/.clasprc.json to the repo. It contains a refresh token that's effectively a long-lived credential. It belongs in GitHub Secrets, never in source control.
We don't share one Google account across the team. The CLASPRC_JSON secret comes from a dedicated account that owns the Apps Script project. Personal accounts churn; the deploy account doesn't.
We don't run tests in this workflow. Apps Script's runtime is different enough from Node that meaningful unit tests are hard. We run logic tests in a separate workflow against a Node port of the shared code, and rely on a manual smoke test after each deploy.
The Payoff
Before this setup, deploying meant: pull latest, hope your clasp token hadn't expired, run clasp push, remember to run clasp deploy with a sensible description, hope you didn't forget the -f. Five steps, five places to fumble.
Now: merge to main, watch the green check, open the add-on. Apps Script stops being a special snowflake — it's just another deploy target behind the same git workflow as everything else you ship.
If you're maintaining a Workspace Add-on and still pushing from your laptop, the migration is a one-afternoon job. The hardest part is finding ~/.clasprc.json.
Have a custom workflow in mind?
We build bespoke Google Workspace add-ons tailored to your business processes. Write to us at support@8apps.co to get started.