How to Set Up Google Apps Script Deployment from GitHub Actions

 

Building a Google Apps Script CI/CD Workflow with GitHub Actions

In this project, the goal was to build a clean and practical CI/CD setup for Google Apps Script using GitHub Actions. The accompanying public GitHub repository, available at google-apps-script-cicd-template, serves as a sanitized and detailed example for explanation purposes.




The goal was simple:
  • Write Apps Script code locally
  • Version it in GitHub
  • Push code to Apps Script with clasp
  • Optionally redeploy an existing Apps Script deployment from GitHub Actions
  • Keep secrets out of the repository

Instead of stopping at a minimal demo, the project was developed into a working example project with a real Apps Script automation flow, a GitHub Actions pipeline, a validation step before deployment, a web app health response, and spreadsheet bootstrap logic. This post walks through the setup from start to finish, including the mistakes encountered and subsequently resolved.

What Was Built

The Apps Script project is a simple sales dashboard automation backed by Google Sheets. It includes several key functions for managing and automating data flows within a Google Sheet environment:

  • setupProject() creates the required sheets (e.g., 'Orders', 'Daily Summary', 'Settings') within the linked Google Sheet.
  • seedDemoOrders() inserts sample data into the 'Orders' sheet, useful for testing and demonstration.
  • refreshDashboard() rebuilds the daily summary based on the latest data in the 'Orders' sheet.
  • emailDailySummary() sends an email with the latest summary rows to designated recipients.
  • createDailyRefreshTrigger() establishes a time-driven trigger to automate the daily refresh process.
  • doGet() returns a JSON health response, providing an endpoint to check the script's operational status and linked spreadsheet details.

The script primarily interacts with three tabs in the Google Sheet: Orders, Daily Summary, and Settings. This gave the project a realistic target for CI/CD instead of a placeholder myFunction(), ensuring that the CI/CD pipeline could handle a more complex and integrated Apps Script solution.

Why the June 2026 Core Service Update Matters

On June 23, 2026, Google announced that Google Apps Script is now officially a Google Workspace core service. Elevating Apps Script to a core service implies a greater commitment from Google towards its stability, security, and integration within the broader Workspace ecosystem.

In practical terms, this means Apps Script now sits in the same general trust category as other core Workspace services instead of feeling like a side utility that admins might hesitate to bless. Google’s announcement says Apps Script is now covered under Google Workspace terms and gets the same broad enterprise posture as other core services, including enterprise-grade data protection, robust administrative controls, and standard technical support.

What "core service" means in practice

For developers, it means Apps Script is easier to justify as an official internal platform for workflow automation, Sheet and Docs customization, internal tools, and lightweight integrations. For admins, it means Apps Script is no longer something they may view as outside the normal Workspace governance model, encouraging greater acceptance and integration of Apps Script solutions within enterprise IT strategies. Admin confidence often determines whether automation is encouraged, tolerated, or blocked.

What changed from an organizational perspective

Before this update, some organizations were cautious about Apps Script because of compliance concerns, support expectations, governance uncertainty, and fear of employees building unofficial automations. Google explicitly says that organizations that previously restricted Apps Script because of compliance, security, or support concerns may want to revisit that decision now.

How this affects the "shadow IT" conversation

While Google does not use the phrase "shadow IT" in the announcement, the designation provides a more structured and officially supported environment for internal automation. When an internal automation platform is not clearly supported, users move workflows into unmanaged tools. Once Apps Script is treated as an official core service, organizations have a stronger reason to keep those automations inside the governed Workspace ecosystem.

Why it fits this CI/CD project

This update makes a GitHub Actions deployment story for Apps Script more compelling. Without enterprise trust, CI/CD for Apps Script can feel like a clever hack. With Apps Script recognized as a core Workspace service, the same setup looks more like a legitimate engineering workflow for a supported internal platform.

Final Repository Structure

The final project structure for this CI/CD setup looks like this:
.
├── .github/workflows/apps-script-cicd.yml
├── .claspignore
├── .clasp.json.example
├── .gitignore
├── Code.js
├── Config.js
├── OrderService.js
├── README.md
├── appsscript.json
├── package.json
└── scripts/validate-apps-script.mjs


Why clasp Was Used

Google Apps Script is convenient inside the browser editor for quick edits, but local development is much better for Git version control, pull requests, CI validation, repeatable deployment, and a cleaner engineering workflow. clasp acts as the bridge, connecting a local development folder to an Apps Script project and enabling files to be pushed from the terminal or CI/CD pipelines.

Before You Start

Enable the Apps Script API

Make sure the Google Apps Script API is enabled for the account. clasp relies on this API to interact with Apps Script projects. This is an easy-to-miss setup step that can block the entire local workflow if skipped.

Install and Authenticate clasp

Install clasp globally on your system using npm:

Bash
npm install -g @google/clasp
Alternatively, manage it as a local project dependency with npm install. Then, authenticate clasp with your Google account:

Bash
npx clasp login
After successful login, clasp creates a local credentials file at ~/.clasprc.json. That file is sensitive and must never be committed to version control.

Step 1: Create the Local Apps Script Project

The process began with a very small Apps Script project to establish the basic structure:

JavaScript
function myFunction() {
}

And a simple manifest:
JSON
{
  "timeZone": "Asia/Kolkata",
  "exceptionLogging": "STACKDRIVER",
  "runtimeVersion": "V8"
}


Step 1.1 : Create or Clone the Apps Script Project

There are two valid starting points for integrating an Apps Script project with clasp.

Option A: Start from an Existing Apps Script Project
If the Apps Script project already exists in the browser editor, it can be cloned locally using its script ID: clasp clone "YOUR_SCRIPT_ID". This creates a .clasp.json file mapping the local folder to the remote project.

Option B: Start From Local Source and Connect Later
This is closer to what was implemented in this repository. .clasp.json exists locally for development but is dynamically managed and ignored by Git. GitHub Actions creates .clasp.json dynamically during deployment using the APPS_SCRIPT_SCRIPT_ID secret. This intentionally avoids committing .clasp.json to the repository so the remote script mapping stays outside version control.

Step 2: Build a Real Example Instead of a Placeholder

To move beyond a basic setup, the script was divided into three distinct files to promote modularity:

  • Code.js: Public entry points and core execution logic (setupProject, seedDemoOrders, refreshDashboard, emailDailySummary, runHealthCheck, doGet).
  • Config.js: Centralizing configuration, environment values, script property names, spreadsheet creation logic, and script lock handling.
  • OrderService.js: Encapsulating sheet operations, creating sheets, writing headers, reading rows, generating summaries, and formatting.

Step 3: Define the Apps Script Manifest

The appsscript.json was updated to use explicit OAuth scopes and web app settings:

JSON
{
  "timeZone": "Asia/Kolkata",
  "exceptionLogging": "STACKDRIVER",
  "runtimeVersion": "V8",
  "oauthScopes": [
    "https://www.googleapis.com/auth/spreadsheets",
    "https://www.googleapis.com/auth/script.send_mail",
    "https://www.googleapis.com/auth/script.scriptapp"
  ],
  "webapp": {
    "executeAs": "USER_DEPLOYING",
    "access": "MYSELF"
  }
}
spreadsheets is needed for creating and updating the sheet. script.send_mail is needed for email dispatch. script.scriptapp is needed for trigger management. The webapp settings configure access rights for doGet().

Step 4: Add Node Tooling for Validation

A package.json file was added to enable local and CI-based validation using Node.js tools. The main command is:

Bash
npm run check
This script (scripts/validate-apps-script.mjs) orchestrates validation steps to check JavaScript syntax (node --check), manifest expectations, required functions, and required OAuth scopes. This fails fast before a broken push reaches production.

Step 5: Control What clasp Pushes

The .claspignore file precisely controls which files are pushed to the Apps Script project:

**/**
!Code.js
!Config.js
!OrderService.js
!appsscript.json
This prevents GitHub workflow files, docs, local config files, and Node tooling from being deployed to the Apps Script server.

Step 6: Git Ignore the Sensitive Files

.gitignore entries were added to prevent sensitive files from being committed:

.clasp.json
.clasprc.json
node_modules/


Why This Repo Ignores .clasp.json

Instead of committing .clasp.json directly to the repository, the GitHub Actions workflow writes it at runtime. The script ID comes from APPS_SCRIPT_SCRIPT_ID (a GitHub Secret), and the auth file comes from CLASPRC_JSON (also a GitHub Secret). This prevents accidental exposure of project IDs and credential files.

Step 7: Set Up GitHub Actions CI/CD

The pipeline is defined in .github/workflows/apps-script-cicd.yml and is structured into two main jobs:

Validate Job

Runs automatically on pull requests and pushes to the main branch. It checks out the repo, sets up Node.js 20, installs dependencies, and runs npm run check.

Deploy Job

Runs automatically after a successful validate job on direct pushes to main. It writes ~/.clasprc.json and .clasp.json dynamically from GitHub Secrets, shows the pending file status, pushes the source with npx clasp push --force, and optionally redeploys an existing deployment.

Step 8: Set the GitHub Secrets

The workflow relies on these GitHub repository secrets: 
GitHub Repo → Settings Secrets and variables → Actions Click New repository secret

  • APPS_SCRIPT_SCRIPT_ID: The unique identifier for your Google Apps Script project.
  • CLASPRC_JSON: The full content of the local ~/.clasprc.json file.
  • APPS_SCRIPT_DEPLOYMENT_ID: (Optional) Used to update an existing Apps Script deployment such as a web app.

Step 9: Connect the Local Repo to GitHub

The local repository was connected to its remote counterpart using(You can use your repo:
https://github.com/<your git repo>/<git-repo-name>
A common issue encountered was pushing to GitHub over HTTPS failing due to incorrect local Git credentials (fatal: could not read Username for '[https://github.com](https://github.com)': Device not configured). The fix was authenticating GitHub properly on the machine.

Troubleshooting the Real Errors Encountered

1. Error: Apps Script health check returned missing script property

JSON
{
  "ok": false,
  "error": "Missing Script Property SPREADSHEET_ID. Add it before running setupProject.",
  "checkedAt": "2026-07-14T10:24:38+05:30"
}
Root Cause: In Config.js, spreadsheetIdProperty had been accidentally changed from a string literal name ('SPREADSHEET_ID') to an actual spreadsheet ID value. The script was looking for a property named after the ID instead of the key SPREADSHEET_ID.

2. Error: Script Properties were added, but setupProject() still failed

Root cause: Adding values in Script Properties is not enough if the pushed code has the wrong config or is an outdated version. Always verify both the Script Properties UI and the code that reads those properties. Run clasp push to ensure the latest code is deployed.

3. Error: zsh: permission denied: ~/.clasprc.json

Root cause: The file path was typed directly into the terminal, causing the shell to interpret it as an executable. Use cat ~/.clasprc.json to read the file content.

4. Error: zsh: command not found: code

Root cause: The VS Code shell command-line utility is not in the terminal's PATH. Run Shell Command: Install 'code' command in PATH from the VS Code Command Palette.

5. Error: fatal: not a git repository

Root cause: Git commands were run in an uninitialized directory. Fix with git init.

6. Error: Git author identity unknown

Root cause: Git requires a name and email. Fix with git config --global user.name "Your Name" and git config --global user.email "your.email@example.com".

7. Error: GitHub repo still showed the empty "Quick setup" page

Root cause: The local repository had commits, but the HTTPS push was failing because GitHub authentication was not set up on the machine. Authenticating and pushing the main branch resolved this.

Step 10: Configure the Apps Script Project

At first, the expectation was to manually set various script properties such as SPREADSHEET_ID, ENVIRONMENT, and SUMMARY_RECIPIENTS. This created friction, so the design was improved to allow the spreadsheet to bootstrap itself if SPREADSHEET_ID is missing.

Step 11: The First Real Bug

As noted in the troubleshooting section, the script property error Missing Script Property SPREADSHEET_ID was a code bug, not an environment issue. spreadsheetIdProperty had been changed in Config.js to an actual spreadsheet ID value rather than the string key.

Step 12: Improve Bootstrap by Auto-Creating the Spreadsheet

To improve the developer experience, bootstrap logic was added in Config.js. If SPREADSHEET_ID does not exist in Script Properties, the script automatically creates a spreadsheet, saves the generated ID into Script Properties, and logs the new URL.

Step 13: Return the Spreadsheet URL from doGet()

doGet() was updated to include additional metadata in the health response:

  • spreadsheetId: The ID of the linked Google Sheet.

  • spreadsheetUrl: The full URL to access the linked Google Sheet.

This makes the web app endpoint highly useful for quick verification during deployment.

Step 14: Why rootDir Exists in .clasp.json

rootDir in .clasp.json is not related to spreadsheet creation or management. It strictly tells clasp where the Apps Script source files live locally relative to .clasp.json. Using the repo root (rootDir: "") is fine because the .claspignore file already filters what gets pushed.

Step 15: Manual Test Flow

After pushing the code, run a manual test flow to confirm functionality:

  1. Run setupProject() to verify sheet and tab creation.
  2. Run seedDemoOrders() to confirm sample data insertion.
  3. Run refreshDashboard() to check daily summary generation.
  4. Optionally run createDailyRefreshTrigger().
  5. Optionally deploy the web app and call doGet() to test the health endpoint.

Step 16: Important Public/Private Boundaries

When making a repository public, strictly observe boundaries to prevent exposing sensitive data:

  • Never Commit: .clasprc.json, .clasp.json, exported credential JSON files, copied OAuth secrets, access tokens, or refresh tokens.
  • Keep in GitHub Secrets: CLASPRC_JSON, APPS_SCRIPT_SCRIPT_ID, and APPS_SCRIPT_DEPLOYMENT_ID.

Step 17: Changes to Consider Before Making the Repo Public(or with in Org)

Before publishing:
  1. Verify .gitignore covers all sensitive files.
  2. Remove hardcoded IDs or secrets.
  3. Replace real spreadsheet IDs, URLs, and email values in documentation/screenshots.
  4. Double-check GitHub Secrets configuration.
  5. Use an isolated demo-only Apps Script project.
  6. Regenerate tokens if anything sensitive was exposed.

Step 18: Lessons Learned

  • Apps Script CI/CD is highly workable with GitHub Actions.
  • clasp
  • perfectly bridges local development with the Apps Script environment.
  • CI Validation before deployment prevents broken pushes to production.
  • Configuration mistakes often mimic platform issues.
  • Returning operational details from doGet() makes debugging easier.
  • Bootstrap automation significantly streamlines the initial setup process.

Step 19: Future Improvements

Next steps for this architecture could include:
  • Separate staging and production Apps Script projects.
  • Deploy only from tags or releases.
  • Add linting to enforce code style.
  • Use GitHub Environments for approval gates.
  • Create a nicer HTML status page for doGet().
  • Add a rollback process for previous deployments.

Conclusion

This project demonstrates a comprehensive workflow for local Apps Script development, GitHub version control, GitHub Actions validation, and automated deployment. The real value of the exercise was discovering where development friction appears handling authentication, securing secrets, managing properties, and fixing configuration mistakes and building a self-bootstrapping template that solves them natively.


Appendix: Commands Used

Bash
npm install
npx clasp login
npx clasp push --force
git remote add origin https://github.com/<yourgitpath>/<git-repo-name>
git push -u origin main

Comments

Popular posts from this blog

Responsive Web Apps using Google Apps Script

Google Apps Script Exception Handling

Google Apps Script Regular Expressions