├── .gitignore ├── src ├── Code.js └── appsscript.json ├── .clasp.json ├── LICENSE.md ├── .github └── workflows │ └── deploy-script.yml └── readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | screenshots/01_Inital.png 3 | screenshots/ 4 | grant.txt 5 | file.txt 6 | -------------------------------------------------------------------------------- /src/Code.js: -------------------------------------------------------------------------------- 1 | function myFunction() { 2 | console.log("Hello Google Apps Script CI/CD!"); 3 | } 4 | -------------------------------------------------------------------------------- /.clasp.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptId": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", 3 | "rootDir": "src" 4 | } 5 | -------------------------------------------------------------------------------- /src/appsscript.json: -------------------------------------------------------------------------------- 1 | { 2 | "timeZone": "America/New_York", 3 | "dependencies": { 4 | }, 5 | "exceptionLogging": "STACKDRIVER", 6 | "runtimeVersion": "V8" 7 | } -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Eric Anastas 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/deploy-script.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Script 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: [main, develop] 7 | release: 8 | types: [published] 9 | schedule: 10 | - cron: "0 0 * * SUN" 11 | 12 | jobs: 13 | deploy: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Install clasp 18 | id: install-clasp 19 | run: sudo npm install @google/clasp@2.4.1 -g 20 | 21 | - name: Write CLASPRC_JSON secret to .clasprc.json file 22 | id: write-clasprc 23 | run: echo "$CLASPRC_JSON_SECRET" >> ~/.clasprc.json 24 | env: 25 | CLASPRC_JSON_SECRET: ${{ secrets.CLASPRC_JSON }} 26 | 27 | - name: Check clasp login status 28 | id: clasp_login 29 | run: clasp login --status 30 | 31 | - name: Save current .clasprc.json contents to CLASPRC_JSON_FILE environment variable 32 | id: save-clasprc 33 | run: | 34 | echo ::add-mask::$(tr -d '\n\r' < ~/.clasprc.json) 35 | echo "CLASPRC_JSON_FILE=$(tr -d '\n\r' < ~/.clasprc.json)" >> $GITHUB_ENV 36 | 37 | - name: Save CLASPRC_JSON_FILE environment variable to CLASPRC_JSON repo secret 38 | id: set-clasprc-secret 39 | if: ${{ env.CLASPRC_JSON_FILE != env.CLASPRC_JSON_SECRET }} 40 | uses: hmanzur/actions-set-secret@v2.0.0 41 | env: 42 | CLASPRC_JSON_SECRET: ${{ secrets.CLASPRC_JSON }} 43 | with: 44 | name: "CLASPRC_JSON" 45 | value: ${{ env.CLASPRC_JSON_FILE }} 46 | repository: ${{ github.repository }} 47 | token: ${{ secrets.REPO_ACCESS_TOKEN }} 48 | 49 | - name: Checkout repo 50 | id: checkout-repo 51 | if: ${{github.event_name != 'schedule' }} 52 | uses: actions/checkout@v2 53 | 54 | - name: Set scriptId in .clasp.json file 55 | id: set-script-id 56 | if: ${{ github.event_name != 'schedule' && env.SCRIPT_ID}} 57 | run: jq '.scriptId = "${{env.SCRIPT_ID}}"' .clasp.json > /tmp/.clasp.json && mv /tmp/.clasp.json .clasp.json 58 | env: 59 | SCRIPT_ID: ${{secrets.SCRIPT_ID}} 60 | 61 | - name: Push script to scripts.google.com 62 | id: clasp-push 63 | if: ${{ github.event_name != 'schedule'}} 64 | run: clasp push -f 65 | 66 | - name: Deploy Script 67 | id: clasp-deploy 68 | if: ${{env.DEPLOYMENT_ID && (github.event_name == 'release' || (github.event_name == 'push' && github.ref == 'refs/heads/main'))}} 69 | run: clasp deploy -i "$DEPLOYMENT_ID" -d "$GITHUB_REF" 70 | env: 71 | DEPLOYMENT_ID: ${{ secrets.DEPLOYMENT_ID }} 72 | # dump-context: 73 | # runs-on: ubuntu-latest 74 | # steps: 75 | # - name: Dump GitHub context 76 | # env: 77 | # GITHUB_CONTEXT: ${{ toJSON(github) }} 78 | # run: echo "$GITHUB_CONTEXT" 79 | # - name: Dump job context 80 | # env: 81 | # JOB_CONTEXT: ${{ toJSON(job) }} 82 | # run: echo "$JOB_CONTEXT" 83 | # - name: Dump steps context 84 | # env: 85 | # STEPS_CONTEXT: ${{ toJSON(steps) }} 86 | # run: echo "$STEPS_CONTEXT" 87 | # - name: Dump runner context 88 | # env: 89 | # RUNNER_CONTEXT: ${{ toJSON(runner) }} 90 | # run: echo "$RUNNER_CONTEXT" 91 | # - name: Dump strategy context 92 | # env: 93 | # STRATEGY_CONTEXT: ${{ toJSON(strategy) }} 94 | # run: echo "$STRATEGY_CONTEXT" 95 | # - name: Dump matrix context 96 | # env: 97 | # MATRIX_CONTEXT: ${{ toJSON(matrix) }} 98 | # run: echo "$MATRIX_CONTEXT" 99 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Deploy Google App Script Action 2 | 3 | [![Deploy Script](https://github.com/SOM-Firmwide/deploy-google-app-script-action/actions/workflows/deploy-script.yml/badge.svg)](https://github.com/SOM-Firmwide/deploy-google-app-script-action/actions/workflows/deploy-script.yml) 4 | 5 | This repository is an example of how to setup an automatic [CI/CD](https://en.wikipedia.org/wiki/CI/CD) process for [Google Apps Script](https://developers.google.com/apps-script) using [GitHub Actions](https://docs.github.com/en/actions). 6 | ## Setup 7 | 8 | ### Setup Project Files 9 | 10 | 1. Install [clasp](https://developers.google.com/apps-script/guides/clasp) on your development machine if not already installed. 11 | 2. Create a local copy of a Google Apps Script project. You may use `clasp create` to create a new project or `clasp clone` to download an existing project. This will create a `.clasp.json` file. 12 | 3. Initialize the project folder as a new Git repo: `git init`. 13 | 1. The `.clasp.json` file created in the prior step MUST be in the root of the Git repository, 14 | 2. `.clasp.json` may point to source files in a sub folder throgh a `rootDir` property. 15 | 4. Copy `.github/workflows/deploy-script.yml` from this repository to the same relative path. 16 | 17 | 18 | #### `.clasp.json` File Format Reference 19 | 20 | { 21 | "scriptId": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"; 22 | "rootDir": "src", 23 | "projectId": "project-id-0000000000000000000", 24 | "fileExtension": "js", 25 | "filePushOrder": ["src/File1.js", "src/File1.js", "src/File1.js"], 26 | "parentId": "XXXXXXXXXXXXXXXXXXXXXX" 27 | } 28 | 29 | 30 | ### Setup Git Repository 31 | 32 | 1. Stage files: `git add .` 33 | 2. Commit files: `git commit -m "first commit"` 34 | 3. Create a `develop` branch: `git branch -M develop` 35 | 4. Create a `main` branch: `git branch -M main` 36 | 5. Create a new GitHub repository, and add it as a remote: `git remote add origin git@github.com:account/repo.git` 37 | 6. Push the `main` branch to GitHub: `git push -u origin main` 38 | 7. Push the `develop` branch to GitHub: `git push -u origin develop` 39 | 40 | At this point the workflow will be triggered, but will fail because it is not configured completely. 41 | 42 | ### Set Repository Secrets 43 | 44 | [Github encrypted secrets](https://docs.github.com/en/actions/reference/encrypted-secrets) are used to configure the workflow and can be set from the repository settings page on GitHub. 45 | #### `CLASPRC_JSON` 46 | 47 | The `clasp` command line tool uses a `.clasprc.json` file to store the current login information. The contents of this file need to be added to a `CLASPRC_JSON` secret to allow the workflow to update and deploy scripts. 48 | 49 | 1. Login to clasp as the user that should run the workflow: 50 | 1. Run `clasp login` 51 | 2. A web browser will open asking you to authenticate clasp. Accept this from the account you want the workflow to use. 52 | 2. Open the `.clasprc.json` file that is created in the home directory (`C:\Users\{username}` on windows, and `~/.clasprc.json` on Linux) 53 | 3. Copy the contents of `.clasprc.json` into a new secret named `CLASPRC_JSON` 54 | 55 | #### `REPO_ACCESS_TOKEN` 56 | A GitHub personal access token must be provided to the workfow to allow it to update the `CLASPRC_JSON` secret configured about when tokens expire and refresh. 57 | 58 | 1. Create a new [GitHubpersonal access token](https://github.com/settings/tokens/new) with `repo` scope. 59 | 2. Copy the token into a new secret named `REPO_ACCESS_TOKEN` 60 | 61 | #### `SCRIPT_ID` [OPTIONAL] 62 | 63 | The clasp command line tool identifies the Google Apps Script project to push and deploy too using the `scriptId` property in `.clasp.json`. You may leave this value hard coded in `.clasp.json` or you may have this set dynamically. To specify the target script dynamically add a `SCRIPT_ID` secret to the repository. This will cause the workflow to override whatever literal scriptId value is in `.clasp.json` 64 | 65 | #### `DEPLOYMENT_ID` [OPTIONAL] 66 | 67 | The workflow can automatically deploy the script when the `main` branch is pushed to github. 68 | 69 | 1. Determine the ID of the deployment you want 70 | 1. Create a new deployment by running `clasp deploy` or on https://scripts.google.com. 71 | 2. Find the deploymen id by running `clasp deployments` or checking the projet settings on https://scripts.google.com. 72 | 2. Add the desired deployment id to a secret naned `DEPLOYMENT_ID` 73 | ## Usage 74 | 75 | - Pushing to either the `main` or `develop` branches on github will automatically trigger the workflow to push the code to the `HEAD` deployment on https://scripts.google.com` 76 | - If the `DEPLOYMENT_ID` secret has been setup pushing to `main` will also deploy the script to the specified deployment. 77 | 78 | ## Updating `.clasprc.json` 79 | 80 | The `.clasprc.json` file that stores the authentication information contains a `access_token` which expires at the specified `expiry_date` and a `refresh_token` that can be used to request a new `access_token`. These tokens will change over time, but the workflow should update the `CLASPRC_JSON` repository secret. 81 | 82 | However, there are [conditions where the refresh token may also expire](https://developers.google.com/identity/protocols/oauth2#expiration). So in addition to the push triggers the workflow is also configured to automatically attempt to login to clasp once a week which will confirm the authentication is still working and potentially refresh and save new tokens. 83 | 84 | ### `.clasprc.json` File Format Reference 85 | 86 | { 87 | "access_token": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", 88 | "refresh_token": "YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY", 89 | "scope": "https://www.googleapis.com/auth/script.projects https://www.googleapis.com/auth/script ...", 90 | "token_type": "Bearer", 91 | "expiry_date": 0000000000000 92 | } 93 | 94 | ## GCP Service Accounts 95 | 96 | The whole system described here copying the credentials out of `.clasprc.json` and using a scheduled trigger to automatically update the tokens on a regular basis is a hack. 97 | 98 | The "correct" way to setup a server to server connection like is through a GCP service account. It is possible to login clasp using a key file for a service account. However, the [Apps Scripts API](https://developers.google.com/apps-script/api/concepts) does not work with service accounts. 99 | 100 | - [Execution API - cant use service account](https://issuetracker.google.com/issues/36763096) 101 | - [Can the Google Apps Script Execution API be called by a service account?](https://stackoverflow.com/questions/33306299/can-the-google-apps-script-execution-api-be-called-by-a-service-account) 102 | 103 | ## Related Issues 104 | 105 | - [Provide instructions for deploying via CI #707](https://github.com/google/clasp/issues/707) 106 | - [Handle rc files prefering local over global to make clasp more CI friendly #486](https://github.com/google/clasp/pull/486) 107 | - [Integration with CI pipeline and Jenkins #524](https://github.com/google/clasp/issues/524) 108 | - [How to use a service account for CI deployments #225](https://github.com/google/clasp/issues/225) 109 | 110 | ## Reference 111 | 112 | - [Advanced Clasp Docs](https://github.com/google/clasp/tree/master/docs) 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | --------------------------------------------------------------------------------