# Shipping a Google Workspace Add-on with clasp and GitHub Actions

> A step-by-step guide to deploying Apps Script add-ons straight from a git push — install clasp, wire up .clasp.json, and let GitHub Actions handle the rest.

- Source: https://www.8apps.co/blog/shipping-google-workspace-addon-clasp-github-actions
- Markdown: https://www.8apps.co/blog/shipping-google-workspace-addon-clasp-github-actions.md
- Published: 2026-05-07
- Author: 8apps Team
- Language: en

---
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/.../edit` and is identified by a **`scriptId`**.
- `clasp push` uploads the `.gs`, `.html`, and `appsscript.json` files in a local directory into that script project, replacing the server-side copy.
- `clasp pull` does the reverse — handy if someone edited code in the Apps Script web editor and you want to bring those changes back to git.
- `clasp deploy` cuts a versioned, named deployment — the one the add-on store and your end users actually run. Without it, `clasp push` only updates the `@HEAD` version 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:

```bash
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](https://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:

```bash
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:

```bash
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:

```json
// .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 omit `rootDir`, clasp uses the current directory, which usually means it tries to upload your `node_modules` and `.git` folders. Always set it explicitly.

A couple of optional fields you may run into:

- **`scriptExtensions`** and **`htmlExtensions`** let you customize which file extensions clasp treats as server code or HTML.
- **`filePushOrder`** forces specific files to be pushed before others — only relevant if you have load-order dependencies between `.gs` files.

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:

- **`.gs`** files are Apps Script server code. clasp converts them transparently to/from the editor's native format.
- **`.html`** files are templates served via `HtmlService.createHtmlOutputFromFile()`.
- **`appsscript.json`** is 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:

```bash
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:

```json
{
  "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:

1. On a workstation, run `clasp login` (skip if you already did Step 1).
2. `cat ~/.clasprc.json` and copy the entire output.
3. In your repo on GitHub: **Settings → Secrets and variables → Actions → New repository secret**.
4. **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`:

```yaml
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 to `main` triggers a deploy. `workflow_dispatch` adds 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.json` so clasp finds it where it expects. Don't `cat` or `echo` the secret anywhere else; GitHub masks it automatically only when used through `${{ secrets.* }}`.
- **`clasp push -f`** — uploads everything in `rootDir` to the Apps Script project. The `-f` is 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 push` only 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](mailto:support@8apps.co) to get started.
