From 69ef59d1934753e64e81c646f97e2113c9255c6d Mon Sep 17 00:00:00 2001 From: Benjamin Sayaque <91118734+Benjamin-Sayaque@users.noreply.github.com> Date: Tue, 9 Jun 2026 15:42:01 +0200 Subject: [PATCH 01/12] Document CI/CD workflow setup --- .clasp.json.example | 4 + .claspignore | 35 ++++ .github/scripts/prepare-tests.sh | 74 +++++++++ .github/workflows/test.yml | 266 +++++++++++++++++++++++++++++++ .gitignore | 9 ++ README.md | 156 ++++++++++++++++++ appsscript.json | 12 ++ src/code.gs | 3 +- 8 files changed, 558 insertions(+), 1 deletion(-) create mode 100644 .clasp.json.example create mode 100644 .claspignore create mode 100755 .github/scripts/prepare-tests.sh create mode 100644 .github/workflows/test.yml create mode 100644 .gitignore create mode 100644 appsscript.json diff --git a/.clasp.json.example b/.clasp.json.example new file mode 100644 index 0000000..02d2a46 --- /dev/null +++ b/.clasp.json.example @@ -0,0 +1,4 @@ +{ + "scriptId": "YOUR_SCRIPT_ID_HERE", + "rootDir": "." +} diff --git a/.claspignore b/.claspignore new file mode 100644 index 0000000..51dee08 --- /dev/null +++ b/.claspignore @@ -0,0 +1,35 @@ +# Ignore everything by default so Apps Script receives only the manifest and src/. +** + +# Required Apps Script manifest. +!appsscript.json + +# Application source files. +!src/ +!src/** + +# Documentation files. +README.md +LICENSE + +# CI and clasp template files. +.github/ +.github/** +.clasp.json.example + +# Future Node.js tooling and dependency files. +package.json +package-lock.json +npm-shrinkwrap.json +yarn.lock +pnpm-lock.yaml +pnpm-workspace.yaml +node_modules/ +node_modules/** +.npmrc +.yarnrc +.yarnrc.yml +.pnpm-store/ +.pnpm-store/** +.yarn/ +.yarn/** diff --git a/.github/scripts/prepare-tests.sh b/.github/scripts/prepare-tests.sh new file mode 100755 index 0000000..295ef8a --- /dev/null +++ b/.github/scripts/prepare-tests.sh @@ -0,0 +1,74 @@ +#!/usr/bin/env bash +set -euo pipefail + +TEST_FILE="src/testFunctions.gs" +ENABLE_API_KEY_TESTS="${ENABLE_API_KEY_TESTS:-false}" +ENABLE_VERTEX_AI_TESTS="${ENABLE_VERTEX_AI_TESTS:-false}" +OPEN_AI_API_KEY="${OPEN_AI_API_KEY:-}" +GEMINI_API_KEY="${GEMINI_API_KEY:-}" +VERTEX_AI_PROJECT_ID="${VERTEX_AI_PROJECT_ID:-}" +VERTEX_AI_LOCATION="${VERTEX_AI_LOCATION:-global}" + +if [ ! -f "$TEST_FILE" ]; then + echo "::error::${TEST_FILE} not found; cannot prepare remote Apps Script tests." + exit 1 +fi + +if [ "$ENABLE_API_KEY_TESTS" = "true" ]; then + missing=() + if [ -z "$OPEN_AI_API_KEY" ]; then + missing+=("OPEN_AI_API_KEY") + fi + if [ -z "$GEMINI_API_KEY" ]; then + missing+=("GEMINI_API_KEY") + fi + + if [ "${#missing[@]}" -gt 0 ]; then + printf '::error::Cannot prepare API key tests because required environment variable(s) are missing: %s\n' "${missing[*]}" + exit 1 + fi +else + echo "Skipping API key injection: API key tests are disabled." +fi + +if [ "$ENABLE_VERTEX_AI_TESTS" = "true" ]; then + if [ -z "$VERTEX_AI_PROJECT_ID" ]; then + echo "::error::Cannot prepare Vertex AI tests because VERTEX_AI_PROJECT_ID is missing." + exit 1 + fi +else + echo "Skipping Vertex AI configuration injection: Vertex AI tests are disabled." +fi + +tmp_file="$(mktemp)" +node <<'NODE' > "$tmp_file" +const declarations = { + OPEN_AI_API_KEY: process.env.OPEN_AI_API_KEY || '', + GEMINI_API_KEY: process.env.GEMINI_API_KEY || '', + VERTEX_AI_PROJECT_ID: process.env.VERTEX_AI_PROJECT_ID || '', + VERTEX_AI_LOCATION: process.env.VERTEX_AI_LOCATION || 'global', +}; + +for (const [name, value] of Object.entries(declarations)) { + process.stdout.write(`const ${name} = ${JSON.stringify(value)};\n`); +} +NODE + +cat "$TEST_FILE" >> "$tmp_file" +mv "$tmp_file" "$TEST_FILE" + +if [ "$ENABLE_VERTEX_AI_TESTS" = "true" ]; then + cat >> "$TEST_FILE" <<'EOF_VERTEX_TEST' + +// Generated by .github/scripts/prepare-tests.sh for CI-only Vertex AI remote test execution. +function testVertexAISimpleChat() { + GenAIApp.setGeminiAuth(VERTEX_AI_PROJECT_ID, VERTEX_AI_LOCATION); + const chat = GenAIApp.newChat(); + chat.addMessage("Reply with exactly: Vertex AI test passed"); + const response = chat.run({ model: GEMINI_MODEL, max_tokens: 64 }); + console.log(`Vertex AI simple chat response:\n${response}`); +} +EOF_VERTEX_TEST +fi + +echo "Prepared ${TEST_FILE} for remote Apps Script tests." diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..92ab81a --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,266 @@ +name: Test + +on: + workflow_dispatch: + inputs: + enable-api-key-tests: + description: "Run Apps Script tests that require OPEN_AI_API_KEY and GEMINI_API_KEY secrets" + required: false + default: false + type: boolean + enable-vertex-ai-tests: + description: "Run Apps Script tests that require Vertex AI configuration secrets" + required: false + default: false + type: boolean + pull_request: + branches: + - main + push: + branches: + - main + +env: + ENABLE_API_KEY_TESTS: ${{ github.event_name == 'workflow_dispatch' && inputs['enable-api-key-tests'] || false }} + ENABLE_VERTEX_AI_TESTS: ${{ github.event_name == 'workflow_dispatch' && inputs['enable-vertex-ai-tests'] || false }} + AUTH_TESTS_REQUESTED: ${{ github.event_name == 'workflow_dispatch' && (inputs['enable-api-key-tests'] || inputs['enable-vertex-ai-tests']) || false }} + API_KEY_TEST_FUNCTIONS: ${{ vars.API_KEY_TEST_FUNCTIONS || 'testSimpleChatInstance' }} + VERTEX_AI_TEST_FUNCTIONS: ${{ vars.VERTEX_AI_TEST_FUNCTIONS || 'testVertexAISimpleChat' }} + VERTEX_AI_LOCATION: ${{ vars.VERTEX_AI_LOCATION || secrets.VERTEX_AI_LOCATION || 'global' }} + +jobs: + syntax-validation: + name: Syntax validation + runs-on: ubuntu-latest + + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + + - name: Validate Apps Script source syntax + shell: bash + run: | + set -euo pipefail + mapfile -t files < <(find src -type f -name '*.gs' -print | sort) + + if [ "${#files[@]}" -eq 0 ]; then + echo "No .gs files found under src/; nothing to validate." + exit 0 + fi + + tmp_dir="$(mktemp -d)" + trap 'rm -rf "$tmp_dir"' EXIT + + echo "Validating Apps Script source files with node --check:" + for file in "${files[@]}"; do + echo "- ${file}" + tmp_file="${tmp_dir}/$(basename "${file%.gs}").js" + cp "$file" "$tmp_file" + node --check "$tmp_file" + done + + apps-script-tests: + name: Apps Script remote tests + runs-on: ubuntu-latest + needs: syntax-validation + outputs: + clasp-available: ${{ steps.clasp-setup.outputs.available }} + clasp-authenticated: ${{ steps.clasp-auth.outputs.authenticated }} + + env: + HAS_CLASPRC_JSON: ${{ secrets.CLASPRC_JSON != '' }} + HAS_SCRIPT_ID: ${{ secrets.SCRIPT_ID != '' }} + HAS_OPEN_AI_API_KEY: ${{ secrets.OPEN_AI_API_KEY != '' }} + HAS_GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY != '' }} + HAS_VERTEX_AI_PROJECT_ID: ${{ secrets.VERTEX_AI_PROJECT_ID != '' }} + HAS_VERTEX_AI_SERVICE_ACCOUNT_JSON: ${{ secrets.VERTEX_AI_SERVICE_ACCOUNT_JSON != '' }} + CLASPRC_JSON: ${{ secrets.CLASPRC_JSON }} + SCRIPT_ID: ${{ secrets.SCRIPT_ID }} + OPEN_AI_API_KEY: ${{ secrets.OPEN_AI_API_KEY }} + GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} + VERTEX_AI_PROJECT_ID: ${{ secrets.VERTEX_AI_PROJECT_ID }} + VERTEX_AI_SERVICE_ACCOUNT_JSON: ${{ secrets.VERTEX_AI_SERVICE_ACCOUNT_JSON }} + + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + + - name: Mask configured secret values + shell: bash + run: | + set -euo pipefail + for value in "$CLASPRC_JSON" "$SCRIPT_ID" "$OPEN_AI_API_KEY" "$GEMINI_API_KEY" "$VERTEX_AI_PROJECT_ID" "$VERTEX_AI_SERVICE_ACCOUNT_JSON"; do + if [ -n "$value" ]; then + echo "::add-mask::$value" + fi + done + + - name: Install clasp + run: npm install --global @google/clasp + + - name: Validate API key test secrets + if: env.ENABLE_API_KEY_TESTS == 'true' + shell: bash + run: | + set -euo pipefail + missing=() + if [ "$HAS_OPEN_AI_API_KEY" != "true" ]; then + missing+=("OPEN_AI_API_KEY") + fi + if [ "$HAS_GEMINI_API_KEY" != "true" ]; then + missing+=("GEMINI_API_KEY") + fi + + if [ "${#missing[@]}" -gt 0 ]; then + printf '::error::API key tests were requested but required secret(s) are missing: %s\n' "${missing[*]}" + exit 1 + fi + + echo "API key test secrets are configured." + + - name: Validate Vertex AI test secrets + if: env.ENABLE_VERTEX_AI_TESTS == 'true' + shell: bash + run: | + set -euo pipefail + missing=() + if [ "$HAS_VERTEX_AI_PROJECT_ID" != "true" ]; then + missing+=("VERTEX_AI_PROJECT_ID") + fi + if [ -z "$VERTEX_AI_LOCATION" ]; then + missing+=("VERTEX_AI_LOCATION repository variable or secret") + fi + if [ "$HAS_VERTEX_AI_SERVICE_ACCOUNT_JSON" != "true" ]; then + missing+=("VERTEX_AI_SERVICE_ACCOUNT_JSON") + fi + + if [ "${#missing[@]}" -gt 0 ]; then + printf '::error::Vertex AI tests were requested but required configuration value(s) are missing: %s\n' "${missing[*]}" + exit 1 + fi + + echo "Vertex AI test configuration is present." + + - name: Prepare clasp project and credentials + id: clasp-setup + shell: bash + run: | + set -euo pipefail + + if [ "$HAS_CLASPRC_JSON" != "true" ]; then + echo "CLASPRC_JSON secret not configured - skipping clasp setup." + echo "available=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + if [ "$HAS_SCRIPT_ID" != "true" ]; then + echo "SCRIPT_ID secret not configured - skipping clasp setup." + echo "available=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + printf '%s' "$CLASPRC_JSON" > "$HOME/.clasprc.json" + chmod 600 "$HOME/.clasprc.json" + + node -e 'const fs = require("fs"); const scriptId = process.env.SCRIPT_ID; fs.writeFileSync(".clasp.json", JSON.stringify({ scriptId, rootDir: "." }, null, 2) + "\n");' + + echo "Generated .clasp.json from SCRIPT_ID and restored ~/.clasprc.json from CLASPRC_JSON." + echo "available=true" >> "$GITHUB_OUTPUT" + + - name: Validate clasp authentication + id: clasp-auth + if: steps.clasp-setup.outputs.available == 'true' + shell: bash + run: | + set +e + output="$(clasp login --status 2>&1)" + status=$? + set -e + + if [ "$status" -ne 0 ]; then + echo "$output" + if [ "$AUTH_TESTS_REQUESTED" = "true" ]; then + echo "::error::Clasp credentials are present but authentication failed - credentials may be expired or revoked. Please regenerate CLASPRC_JSON." + else + echo "Clasp credentials are present but authentication failed; no auth tests were requested, so remote Apps Script tests will be skipped." + fi + echo "authenticated=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + echo "$output" + echo "clasp authentication is valid." + echo "authenticated=true" >> "$GITHUB_OUTPUT" + + - name: Fail when requested auth tests cannot use clasp + if: env.AUTH_TESTS_REQUESTED == 'true' && steps.clasp-auth.outputs.authenticated != 'true' + shell: bash + run: | + echo "::error::Auth tests were requested but clasp authentication failed. Ensure CLASPRC_JSON and SCRIPT_ID secrets are configured and credentials are valid." + exit 1 + + - name: Skip API key tests + if: env.ENABLE_API_KEY_TESTS != 'true' + run: | + echo "Skipping API key tests: not enabled via workflow input." + + - name: Skip Vertex AI tests + if: env.ENABLE_VERTEX_AI_TESTS != 'true' + run: | + echo "Skipping Vertex AI tests: not enabled via workflow input." + + - name: Skip remote Apps Script tests when clasp is unavailable + if: env.AUTH_TESTS_REQUESTED != 'true' && steps.clasp-auth.outputs.authenticated != 'true' + run: | + echo "No auth tests requested and clasp is unavailable; skipping clasp-dependent remote Apps Script tests." + + - name: Prepare remote test source + if: env.AUTH_TESTS_REQUESTED == 'true' && steps.clasp-auth.outputs.authenticated == 'true' + shell: bash + run: .github/scripts/prepare-tests.sh + + - name: Push source to Apps Script + if: env.AUTH_TESTS_REQUESTED == 'true' && steps.clasp-auth.outputs.authenticated == 'true' + run: clasp push --force + + - name: Run API key Apps Script tests + if: env.ENABLE_API_KEY_TESTS == 'true' && steps.clasp-auth.outputs.authenticated == 'true' + shell: bash + run: | + set -euo pipefail + IFS=',' read -ra functions <<< "$API_KEY_TEST_FUNCTIONS" + for function_name in "${functions[@]}"; do + function_name="${function_name//[[:space:]]/}" + if [ -z "$function_name" ]; then + continue + fi + + echo "Running API key Apps Script test function: ${function_name}" + clasp run "$function_name" + done + + - name: Run Vertex AI Apps Script tests + if: env.ENABLE_VERTEX_AI_TESTS == 'true' && steps.clasp-auth.outputs.authenticated == 'true' + shell: bash + run: | + set -euo pipefail + IFS=',' read -ra functions <<< "$VERTEX_AI_TEST_FUNCTIONS" + for function_name in "${functions[@]}"; do + function_name="${function_name//[[:space:]]/}" + if [ -z "$function_name" ]; then + continue + fi + + echo "Running Vertex AI Apps Script test function: ${function_name}" + clasp run "$function_name" + done diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..09c562b --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +# Local clasp configuration generated from the SCRIPT_ID secret in CI. +.clasp.json + +# Local dependencies and tooling artifacts +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* diff --git a/README.md b/README.md index 20cab1a..c9f76ed 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ The **GenAIApp** library is a Google Apps Script library designed for creating, - [Features](#features) - [Prerequisites](#prerequisites) - [Installation](#installation) +- [Continuous Integration (CI/CD)](#continuous-integration-cicd) - [Usage](#usage) - [Setting API Keys](#setting-api-keys) - [Creating a New Chat](#creating-a-new-chat) @@ -74,6 +75,161 @@ Ensure to link your Google Apps Script project to a GCP project with Vertex AI e To start using the library, include the **GenAIApp** code in your Google Apps Script project environment. +## Continuous Integration (CI/CD) + +This repository includes GitHub Actions, clasp, and Apps Script project infrastructure for CI-based validation and optional remote test execution. The workflow lives at `.github/workflows/test.yml`; helper files include `appsscript.json`, `.claspignore`, `.clasp.json.example`, and `.github/scripts/prepare-tests.sh`. The real `.clasp.json` file is intentionally ignored by Git and generated dynamically in CI from the `SCRIPT_ID` secret before `clasp push` or `clasp run` is executed. + +### What the workflow validates + +The **Test** workflow has two layers: + +1. **Syntax validation** always runs. It checks out the repository, sets up Node.js, lists every `.gs` file under `src/`, copies each file to a temporary `.js` file, and runs `node --check` to catch JavaScript syntax errors. This job does **not** require clasp credentials, Apps Script authentication, script IDs, or API keys. +2. **Optional remote Apps Script tests** run only when explicitly enabled from the manual workflow inputs. When enabled and correctly configured, CI installs clasp, recreates clasp credentials, generates `.clasp.json`, pushes the repository to the Apps Script test deployment, and invokes configured Apps Script test functions with `clasp run`. + +### Workflow triggers + +The workflow runs automatically for: + +- Pushes to the `main` branch. +- Pull requests targeting the `main` branch. + +Automatic runs keep remote/authenticated tests disabled by default, so they provide always-on syntax validation without requiring repository secrets to be available to every event. + +To run the workflow manually: + +1. Open the repository on GitHub. +2. Go to **Actions**. +3. Select the **Test** workflow. +4. Click **Run workflow**. +5. Choose the branch and set `enable-api-key-tests` and/or `enable-vertex-ai-tests` to `true` only for the remote test modes you want to run. + +### Required GitHub secrets + +Configure secrets in **GitHub repository → Settings → Secrets and variables → Actions → Repository secrets**. Never commit these values to the repository. + +| Secret or variable | Required when | How to obtain it | +| --- | --- | --- | +| `CLASPRC_JSON` | Any remote Apps Script test mode is enabled | Install and authenticate clasp locally with `clasp login`, then copy the full contents of `~/.clasprc.json` into a GitHub repository secret. | +| `SCRIPT_ID` | Any remote Apps Script test mode is enabled | Open the Apps Script project used for CI tests and copy its script ID from **Project Settings → Script ID**, or from the Apps Script URL. | +| `OPEN_AI_API_KEY` | `enable-api-key-tests=true` | Create/copy an OpenAI API key from your OpenAI account and store only the key value as the secret. | +| `GEMINI_API_KEY` | `enable-api-key-tests=true` | Create/copy a Gemini API key from Google AI Studio or the Google Cloud API credentials page and store only the key value as the secret. | +| `VERTEX_AI_PROJECT_ID` | `enable-vertex-ai-tests=true` | Use the Google Cloud project ID for the standard GCP project linked to the Apps Script project and with Vertex AI enabled. | +| `VERTEX_AI_LOCATION` | `enable-vertex-ai-tests=true` unless you accept the workflow default of `global` | Configure it as either a repository variable or secret. Use the Vertex AI location you want the tests to call, such as `global` or `us-central1`. | +| `VERTEX_AI_SERVICE_ACCOUNT_JSON` | `enable-vertex-ai-tests=true` | Create a Google Cloud service account with the permissions your Vertex AI test flow requires, create a JSON key if your organization allows key-based CI credentials, and store the complete JSON document as the secret. | + +> **Note:** The current Apps Script Vertex AI smoke test authenticates through the linked Apps Script/GCP project via `ScriptApp.getOAuthToken()` after CI pushes the code. The workflow still validates `VERTEX_AI_SERVICE_ACCOUNT_JSON` when Vertex AI tests are requested so the repository has an explicit place for future Vertex AI CI credential needs. + +#### Creating and maintaining `CLASPRC_JSON` + +`CLASPRC_JSON` is the serialized clasp OAuth credential file used by CI. To generate it: + +1. Install clasp locally if needed: `npm install --global @google/clasp`. +2. Run `clasp login`. +3. Complete the browser-based Google OAuth flow with the Google account that has access to the Apps Script test project. +4. Locate the generated file at `~/.clasprc.json`. +5. Copy the entire JSON file contents. +6. Create or update the GitHub repository secret named `CLASPRC_JSON` with that JSON content. + +Regenerate and update `CLASPRC_JSON` when: + +- CI reports clasp authentication failures, invalid credentials, or expired/revoked tokens. +- You change the Google account used for CI deployments. +- Required Apps Script OAuth scopes change and clasp needs to reauthorize. +- Google revokes the credential, an administrator resets access, or token refresh starts failing. +- Credentials expire. This can happen without warning, so periodic regeneration may be required even if no repository files changed. + +### Workflow inputs and behavior + +| Manual input | Default | What it controls | Required configuration | +| --- | --- | --- | --- | +| `enable-api-key-tests` | `false` | Runs remote Apps Script tests that use the OpenAI and Gemini API-key paths. | `CLASPRC_JSON`, `SCRIPT_ID`, `OPEN_AI_API_KEY`, and `GEMINI_API_KEY`. | +| `enable-vertex-ai-tests` | `false` | Runs remote Apps Script tests that use the Vertex AI authentication path. | `CLASPRC_JSON`, `SCRIPT_ID`, `VERTEX_AI_PROJECT_ID`, `VERTEX_AI_LOCATION`, and `VERTEX_AI_SERVICE_ACCOUNT_JSON`. | + +Skip and failure behavior is intentional: + +- If a mode is disabled, the workflow logs a message such as `Skipping API key tests: not enabled via workflow input` or `Skipping Vertex AI tests: not enabled via workflow input`. +- If both auth modes are disabled and clasp credentials are missing or invalid, the remote test steps are skipped and syntax validation can still pass. +- If any auth mode is enabled and `CLASPRC_JSON`, `SCRIPT_ID`, or clasp authentication is missing/invalid, the workflow fails with an actionable message telling you to configure or regenerate clasp credentials. +- If API-key tests are enabled but `OPEN_AI_API_KEY` or `GEMINI_API_KEY` is missing, the workflow fails before pushing or running remote tests. +- If Vertex AI tests are enabled but the required Vertex AI configuration is missing, the workflow fails before pushing or running remote tests. + +Example scenarios: + +- **Syntax validation only:** leave both inputs set to `false`. This is the default for manual runs and automatic `main`/PR runs. +- **Run only API-key tests:** set `enable-api-key-tests=true` and `enable-vertex-ai-tests=false`; configure `CLASPRC_JSON`, `SCRIPT_ID`, `OPEN_AI_API_KEY`, and `GEMINI_API_KEY`. +- **Run only Vertex AI tests:** set `enable-api-key-tests=false` and `enable-vertex-ai-tests=true`; configure `CLASPRC_JSON`, `SCRIPT_ID`, `VERTEX_AI_PROJECT_ID`, `VERTEX_AI_LOCATION`, and `VERTEX_AI_SERVICE_ACCOUNT_JSON`. +- **Run all remote tests:** set both inputs to `true` and configure every secret listed above. + +The remote test functions can be customized with repository variables: + +- `API_KEY_TEST_FUNCTIONS`: comma-separated Apps Script function names for API-key tests. Defaults to `testSimpleChatInstance`. +- `VERTEX_AI_TEST_FUNCTIONS`: comma-separated Apps Script function names for Vertex AI tests. Defaults to `testVertexAISimpleChat`, which is generated by the CI prepare script. + +### CI to local setup mapping + +Local Apps Script development and CI use the same script-level variable names, but they are populated differently. Locally, define the values directly in your Apps Script project or test file. In CI, `.github/scripts/prepare-tests.sh` injects `const` declarations at the top of `src/testFunctions.gs` in the temporary workflow checkout immediately before `clasp push`; those changes are not committed. + +| GitHub secret/variable | Local Apps Script variable or setting | Used by | +| --- | --- | --- | +| `OPEN_AI_API_KEY` | `const OPEN_AI_API_KEY = '...'` | `GenAIApp.setOpenAIAPIKey(OPEN_AI_API_KEY)` in tests. | +| `GEMINI_API_KEY` | `const GEMINI_API_KEY = '...'` | `GenAIApp.setGeminiAPIKey(GEMINI_API_KEY)` in tests. | +| `VERTEX_AI_PROJECT_ID` | `const VERTEX_AI_PROJECT_ID = '...'` | `GenAIApp.setGeminiAuth(VERTEX_AI_PROJECT_ID, VERTEX_AI_LOCATION)` in Vertex AI tests. | +| `VERTEX_AI_LOCATION` | `const VERTEX_AI_LOCATION = 'global'` or another region | Vertex AI location passed to `setGeminiAuth`. | +| `SCRIPT_ID` | Local `.clasp.json` `scriptId` | Determines which Apps Script project clasp pushes/runs against. | +| `CLASPRC_JSON` | Local `~/.clasprc.json` | Authenticates clasp as the selected Google account. | +| `VERTEX_AI_SERVICE_ACCOUNT_JSON` | Service account JSON kept outside Apps Script source | Reserved/validated for Vertex AI CI credentials and future workflow expansion. | + +Important local/CI differences: + +- Locally, you may already have `.clasp.json` and `~/.clasprc.json`; CI recreates both from secrets every run. +- Locally, test constants can be manually defined; CI injects them automatically and discards the modified test file when the runner is destroyed. +- CI uses `.claspignore` so only `appsscript.json` and `src/` contents are pushed to Apps Script. +- CI remote tests run in the Apps Script environment through `clasp run`, so failures may involve Apps Script permissions, OAuth scopes, linked Google Cloud project configuration, or API quotas in addition to JavaScript errors. + +### Troubleshooting CI/CD + +#### Clasp credential failures + +In CI logs, inspect the **Validate clasp authentication** step. Credential problems commonly show up as: + +- `clasp login --status` failures. +- Messages stating that clasp credentials are present but authentication failed, invalid, expired, or revoked. +- A follow-up failure that says auth tests were requested but clasp authentication failed. + +To regenerate `CLASPRC_JSON`: + +1. On your local machine, run `clasp logout` to clear existing clasp credentials. +2. Run `clasp login`. +3. Complete the OAuth flow in the browser. +4. Confirm local authentication works with `clasp login --status`. +5. Open `~/.clasprc.json` and copy the full JSON content. +6. In GitHub, go to **Settings → Secrets and variables → Actions → Repository secrets**. +7. Update the `CLASPRC_JSON` secret with the new JSON content. +8. Re-run the GitHub Actions workflow. + +Before updating CI, it is useful to verify clasp works locally against the intended script: + +```bash +clasp login --status +cat > .clasp.json <<'EOF' +{"scriptId":"YOUR_SCRIPT_ID","rootDir":"."} +EOF +clasp push --dry-run +``` + +#### Common failures and resolutions + +| Symptom in CI logs | Likely cause | Resolution | +| --- | --- | --- | +| `401 Unauthorized`, invalid credentials, or expired token messages | `CLASPRC_JSON` is expired or revoked. | Regenerate `CLASPRC_JSON` with `clasp logout` and `clasp login`, then update the GitHub secret. | +| `Script not found` or permission denied for the script | `SCRIPT_ID` is wrong, or the Google account in `CLASPRC_JSON` does not have access to that Apps Script project. | Confirm the Apps Script project ID and share the project with the clasp-authenticated Google account. | +| `CLASPRC_JSON secret not configured - skipping clasp setup` | The secret is missing. | Add `CLASPRC_JSON` if you want remote Apps Script tests. Leave it unset for syntax-only CI. | +| `SCRIPT_ID secret not configured - skipping clasp setup` | The target Apps Script project ID is missing. | Add the `SCRIPT_ID` repository secret. | +| Missing `OPEN_AI_API_KEY` or `GEMINI_API_KEY` errors | API-key tests were enabled without required API-key secrets. | Add both API-key secrets or disable `enable-api-key-tests`. | +| Missing Vertex AI configuration errors | Vertex AI tests were enabled without required Vertex AI values. | Add `VERTEX_AI_PROJECT_ID`, `VERTEX_AI_LOCATION`, and `VERTEX_AI_SERVICE_ACCOUNT_JSON`, or disable `enable-vertex-ai-tests`. | +| Apps Script OAuth scope errors after `clasp run` | The Apps Script project needs reauthorization after scopes changed. | Re-run local clasp authentication and ensure the Apps Script project has the manifest scopes in `appsscript.json`; regenerate `CLASPRC_JSON` if needed. | +| API quota or permission errors from OpenAI, Gemini, or Vertex AI | External API credentials, linked GCP project, billing, API enablement, or quotas are not ready. | Verify the relevant API key/project/service account locally first, then re-run CI. | + ## Usage ### Setting API Keys diff --git a/appsscript.json b/appsscript.json new file mode 100644 index 0000000..a9e9eee --- /dev/null +++ b/appsscript.json @@ -0,0 +1,12 @@ +{ + "timeZone": "Etc/UTC", + "runtimeVersion": "V8", + "executionApi": { + "access": "ANYONE" + }, + "oauthScopes": [ + "https://www.googleapis.com/auth/cloud-platform", + "https://www.googleapis.com/auth/drive", + "https://www.googleapis.com/auth/script.external_request" + ] +} diff --git a/src/code.gs b/src/code.gs index 80a8303..28b393c 100644 --- a/src/code.gs +++ b/src/code.gs @@ -1050,9 +1050,10 @@ const GenAIApp = (function () { mimeType: mimeType, blob: blob }; - } + }; } } + } // End Chat class. /** * @class From 53015d29232cf7dcb8cb6d3a051796353b46509c Mon Sep 17 00:00:00 2001 From: Paul Aubry Date: Tue, 9 Jun 2026 16:19:41 +0200 Subject: [PATCH 02/12] bracket fix --- src/code.gs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/code.gs b/src/code.gs index 28b393c..bab9502 100644 --- a/src/code.gs +++ b/src/code.gs @@ -320,6 +320,8 @@ const GenAIApp = (function () { if (containerId) { this._codeInterpreterContainerId = containerId; } + return this; + }; /** OPTIONAL * @@ -1052,7 +1054,6 @@ const GenAIApp = (function () { }; }; } - } } // End Chat class. /** From 8c4e2cb1ae234c8d56e4d7b81fbe4faa014d9020 Mon Sep 17 00:00:00 2001 From: aubrypaul <62645653+aubrypaul@users.noreply.github.com> Date: Tue, 9 Jun 2026 16:41:59 +0200 Subject: [PATCH 03/12] Update appsscript.json Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- appsscript.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appsscript.json b/appsscript.json index a9e9eee..5e8fd4b 100644 --- a/appsscript.json +++ b/appsscript.json @@ -2,7 +2,7 @@ "timeZone": "Etc/UTC", "runtimeVersion": "V8", "executionApi": { - "access": "ANYONE" + "access": "MYSELF" }, "oauthScopes": [ "https://www.googleapis.com/auth/cloud-platform", From 2893455e445b15039c838dc0b0f44d24e87a96f3 Mon Sep 17 00:00:00 2001 From: aubrypaul <62645653+aubrypaul@users.noreply.github.com> Date: Tue, 9 Jun 2026 16:42:25 +0200 Subject: [PATCH 04/12] Update .github/scripts/prepare-tests.sh Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .github/scripts/prepare-tests.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/scripts/prepare-tests.sh b/.github/scripts/prepare-tests.sh index 295ef8a..76c8b35 100755 --- a/.github/scripts/prepare-tests.sh +++ b/.github/scripts/prepare-tests.sh @@ -66,8 +66,12 @@ function testVertexAISimpleChat() { const chat = GenAIApp.newChat(); chat.addMessage("Reply with exactly: Vertex AI test passed"); const response = chat.run({ model: GEMINI_MODEL, max_tokens: 64 }); + if (String(response).trim() !== "Vertex AI test passed") { + throw new Error(`Unexpected Vertex AI response: ${response}`); + } console.log(`Vertex AI simple chat response:\n${response}`); } +} EOF_VERTEX_TEST fi From 753a7710b90e173e54d55df9b6ea300b663de3ea Mon Sep 17 00:00:00 2001 From: aubrypaul <62645653+aubrypaul@users.noreply.github.com> Date: Tue, 9 Jun 2026 16:42:45 +0200 Subject: [PATCH 05/12] Update .github/workflows/test.yml Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .github/workflows/test.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 92ab81a..cde4ec1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -20,6 +20,9 @@ on: branches: - main +permissions: + contents: read + env: ENABLE_API_KEY_TESTS: ${{ github.event_name == 'workflow_dispatch' && inputs['enable-api-key-tests'] || false }} ENABLE_VERTEX_AI_TESTS: ${{ github.event_name == 'workflow_dispatch' && inputs['enable-vertex-ai-tests'] || false }} From d3300669343cbd277b1302c1442a518b39574918 Mon Sep 17 00:00:00 2001 From: aubrypaul <62645653+aubrypaul@users.noreply.github.com> Date: Tue, 9 Jun 2026 16:43:11 +0200 Subject: [PATCH 06/12] Update .github/workflows/test.yml Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .github/workflows/test.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index cde4ec1..d6571c8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -39,6 +39,8 @@ jobs: steps: - name: Check out repository uses: actions/checkout@v4 + with: + persist-credentials: false - name: Set up Node.js uses: actions/setup-node@v4 From 018aa5aa06fc9df1e867425ca4b9f2621a19eeeb Mon Sep 17 00:00:00 2001 From: Paul Aubry Date: Tue, 9 Jun 2026 16:53:05 +0200 Subject: [PATCH 07/12] fix --- .github/scripts/prepare-tests.sh | 1 - .github/workflows/test.yml | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/scripts/prepare-tests.sh b/.github/scripts/prepare-tests.sh index 76c8b35..fcbcc40 100755 --- a/.github/scripts/prepare-tests.sh +++ b/.github/scripts/prepare-tests.sh @@ -71,7 +71,6 @@ function testVertexAISimpleChat() { } console.log(`Vertex AI simple chat response:\n${response}`); } -} EOF_VERTEX_TEST fi diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d6571c8..06f2092 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -94,6 +94,8 @@ jobs: steps: - name: Check out repository uses: actions/checkout@v4 + with: + persist-credentials: false - name: Set up Node.js uses: actions/setup-node@v4 From 06dc5824041d8826a6e9777f5c1baf4286070811 Mon Sep 17 00:00:00 2001 From: Paul Aubry Date: Tue, 9 Jun 2026 17:12:39 +0200 Subject: [PATCH 08/12] fix --- .github/scripts/prepare-tests.sh | 40 ++------------ .github/workflows/test.yml | 61 ++++++++++++++++++---- appsscript.json | 3 +- src/testFunctions.gs | 90 +++++++++++++++++++++++++++++--- 4 files changed, 139 insertions(+), 55 deletions(-) diff --git a/.github/scripts/prepare-tests.sh b/.github/scripts/prepare-tests.sh index fcbcc40..2a4ac39 100755 --- a/.github/scripts/prepare-tests.sh +++ b/.github/scripts/prepare-tests.sh @@ -28,7 +28,7 @@ if [ "$ENABLE_API_KEY_TESTS" = "true" ]; then exit 1 fi else - echo "Skipping API key injection: API key tests are disabled." + echo "Skipping API key validation: API key tests are disabled." fi if [ "$ENABLE_VERTEX_AI_TESTS" = "true" ]; then @@ -37,41 +37,7 @@ if [ "$ENABLE_VERTEX_AI_TESTS" = "true" ]; then exit 1 fi else - echo "Skipping Vertex AI configuration injection: Vertex AI tests are disabled." + echo "Skipping Vertex AI configuration validation: Vertex AI tests are disabled." fi -tmp_file="$(mktemp)" -node <<'NODE' > "$tmp_file" -const declarations = { - OPEN_AI_API_KEY: process.env.OPEN_AI_API_KEY || '', - GEMINI_API_KEY: process.env.GEMINI_API_KEY || '', - VERTEX_AI_PROJECT_ID: process.env.VERTEX_AI_PROJECT_ID || '', - VERTEX_AI_LOCATION: process.env.VERTEX_AI_LOCATION || 'global', -}; - -for (const [name, value] of Object.entries(declarations)) { - process.stdout.write(`const ${name} = ${JSON.stringify(value)};\n`); -} -NODE - -cat "$TEST_FILE" >> "$tmp_file" -mv "$tmp_file" "$TEST_FILE" - -if [ "$ENABLE_VERTEX_AI_TESTS" = "true" ]; then - cat >> "$TEST_FILE" <<'EOF_VERTEX_TEST' - -// Generated by .github/scripts/prepare-tests.sh for CI-only Vertex AI remote test execution. -function testVertexAISimpleChat() { - GenAIApp.setGeminiAuth(VERTEX_AI_PROJECT_ID, VERTEX_AI_LOCATION); - const chat = GenAIApp.newChat(); - chat.addMessage("Reply with exactly: Vertex AI test passed"); - const response = chat.run({ model: GEMINI_MODEL, max_tokens: 64 }); - if (String(response).trim() !== "Vertex AI test passed") { - throw new Error(`Unexpected Vertex AI response: ${response}`); - } - console.log(`Vertex AI simple chat response:\n${response}`); -} -EOF_VERTEX_TEST -fi - -echo "Prepared ${TEST_FILE} for remote Apps Script tests." +echo "Validated remote Apps Script test configuration without modifying ${TEST_FILE}." diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 06f2092..cb0169c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -3,6 +3,11 @@ name: Test on: workflow_dispatch: inputs: + run_remote_tests: + description: "Run remote Apps Script tests against the configured SCRIPT_ID" + required: false + default: false + type: boolean enable-api-key-tests: description: "Run Apps Script tests that require OPEN_AI_API_KEY and GEMINI_API_KEY secrets" required: false @@ -24,9 +29,9 @@ permissions: contents: read env: - ENABLE_API_KEY_TESTS: ${{ github.event_name == 'workflow_dispatch' && inputs['enable-api-key-tests'] || false }} - ENABLE_VERTEX_AI_TESTS: ${{ github.event_name == 'workflow_dispatch' && inputs['enable-vertex-ai-tests'] || false }} - AUTH_TESTS_REQUESTED: ${{ github.event_name == 'workflow_dispatch' && (inputs['enable-api-key-tests'] || inputs['enable-vertex-ai-tests']) || false }} + ENABLE_API_KEY_TESTS: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.run_remote_tests == 'true' && inputs['enable-api-key-tests'] || false }} + ENABLE_VERTEX_AI_TESTS: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.run_remote_tests == 'true' && inputs['enable-vertex-ai-tests'] || false }} + AUTH_TESTS_REQUESTED: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.run_remote_tests == 'true' && (inputs['enable-api-key-tests'] || inputs['enable-vertex-ai-tests']) || false }} API_KEY_TEST_FUNCTIONS: ${{ vars.API_KEY_TEST_FUNCTIONS || 'testSimpleChatInstance' }} VERTEX_AI_TEST_FUNCTIONS: ${{ vars.VERTEX_AI_TEST_FUNCTIONS || 'testVertexAISimpleChat' }} VERTEX_AI_LOCATION: ${{ vars.VERTEX_AI_LOCATION || secrets.VERTEX_AI_LOCATION || 'global' }} @@ -71,8 +76,12 @@ jobs: apps-script-tests: name: Apps Script remote tests + if: github.event_name == 'workflow_dispatch' && github.event.inputs.run_remote_tests == 'true' runs-on: ubuntu-latest needs: syntax-validation + concurrency: + group: ${{ github.repository }}-${{ github.workflow }}-apps-script-tests + cancel-in-progress: true outputs: clasp-available: ${{ steps.clasp-setup.outputs.available }} clasp-authenticated: ${{ steps.clasp-auth.outputs.authenticated }} @@ -83,13 +92,11 @@ jobs: HAS_OPEN_AI_API_KEY: ${{ secrets.OPEN_AI_API_KEY != '' }} HAS_GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY != '' }} HAS_VERTEX_AI_PROJECT_ID: ${{ secrets.VERTEX_AI_PROJECT_ID != '' }} - HAS_VERTEX_AI_SERVICE_ACCOUNT_JSON: ${{ secrets.VERTEX_AI_SERVICE_ACCOUNT_JSON != '' }} CLASPRC_JSON: ${{ secrets.CLASPRC_JSON }} SCRIPT_ID: ${{ secrets.SCRIPT_ID }} OPEN_AI_API_KEY: ${{ secrets.OPEN_AI_API_KEY }} GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} VERTEX_AI_PROJECT_ID: ${{ secrets.VERTEX_AI_PROJECT_ID }} - VERTEX_AI_SERVICE_ACCOUNT_JSON: ${{ secrets.VERTEX_AI_SERVICE_ACCOUNT_JSON }} steps: - name: Check out repository @@ -106,7 +113,7 @@ jobs: shell: bash run: | set -euo pipefail - for value in "$CLASPRC_JSON" "$SCRIPT_ID" "$OPEN_AI_API_KEY" "$GEMINI_API_KEY" "$VERTEX_AI_PROJECT_ID" "$VERTEX_AI_SERVICE_ACCOUNT_JSON"; do + for value in "$CLASPRC_JSON" "$SCRIPT_ID" "$OPEN_AI_API_KEY" "$GEMINI_API_KEY" "$VERTEX_AI_PROJECT_ID"; do if [ -n "$value" ]; then echo "::add-mask::$value" fi @@ -147,9 +154,6 @@ jobs: if [ -z "$VERTEX_AI_LOCATION" ]; then missing+=("VERTEX_AI_LOCATION repository variable or secret") fi - if [ "$HAS_VERTEX_AI_SERVICE_ACCOUNT_JSON" != "true" ]; then - missing+=("VERTEX_AI_SERVICE_ACCOUNT_JSON") - fi if [ "${#missing[@]}" -gt 0 ]; then printf '::error::Vertex AI tests were requested but required configuration value(s) are missing: %s\n' "${missing[*]}" @@ -231,15 +235,34 @@ jobs: run: | echo "No auth tests requested and clasp is unavailable; skipping clasp-dependent remote Apps Script tests." - - name: Prepare remote test source + - name: Validate remote test configuration if: env.AUTH_TESTS_REQUESTED == 'true' && steps.clasp-auth.outputs.authenticated == 'true' shell: bash run: .github/scripts/prepare-tests.sh - - name: Push source to Apps Script + - name: Push clean source to Apps Script if: env.AUTH_TESTS_REQUESTED == 'true' && steps.clasp-auth.outputs.authenticated == 'true' run: clasp push --force + - name: Provision test script properties + if: env.AUTH_TESTS_REQUESTED == 'true' && steps.clasp-auth.outputs.authenticated == 'true' + shell: bash + run: | + set -euo pipefail + params="$(node -e ' + const properties = {}; + if (process.env.ENABLE_API_KEY_TESTS === "true") { + properties.OPEN_AI_API_KEY = process.env.OPEN_AI_API_KEY; + properties.GEMINI_API_KEY = process.env.GEMINI_API_KEY; + } + if (process.env.ENABLE_VERTEX_AI_TESTS === "true") { + properties.VERTEX_AI_PROJECT_ID = process.env.VERTEX_AI_PROJECT_ID; + properties.VERTEX_AI_LOCATION = process.env.VERTEX_AI_LOCATION; + } + process.stdout.write(JSON.stringify([properties])); + ')" + clasp run setCITestScriptProperties --params "$params" + - name: Run API key Apps Script tests if: env.ENABLE_API_KEY_TESTS == 'true' && steps.clasp-auth.outputs.authenticated == 'true' shell: bash @@ -271,3 +294,19 @@ jobs: echo "Running Vertex AI Apps Script test function: ${function_name}" clasp run "$function_name" done + + - name: Clear test script properties + if: always() && env.AUTH_TESTS_REQUESTED == 'true' && steps.clasp-auth.outputs.authenticated == 'true' + shell: bash + run: | + set -euo pipefail + params="$(node -e ' + const propertyNames = [ + "OPEN_AI_API_KEY", + "GEMINI_API_KEY", + "VERTEX_AI_PROJECT_ID", + "VERTEX_AI_LOCATION" + ]; + process.stdout.write(JSON.stringify([propertyNames])); + ')" + clasp run clearCITestScriptProperties --params "$params" diff --git a/appsscript.json b/appsscript.json index 5e8fd4b..18adf5c 100644 --- a/appsscript.json +++ b/appsscript.json @@ -7,6 +7,7 @@ "oauthScopes": [ "https://www.googleapis.com/auth/cloud-platform", "https://www.googleapis.com/auth/drive", - "https://www.googleapis.com/auth/script.external_request" + "https://www.googleapis.com/auth/script.external_request", + "https://www.googleapis.com/auth/script.storage" ] } diff --git a/src/testFunctions.gs b/src/testFunctions.gs index fdb96a8..4ac5c20 100644 --- a/src/testFunctions.gs +++ b/src/testFunctions.gs @@ -3,6 +3,75 @@ const REASONING_MODEL = "o4-mini"; const GEMINI_MODEL = "gemini-2.5-pro"; const TEST_CODE_INTERPRETER_XLSX_DRIVE_FILE_ID = ""; const TEST_CODE_INTERPRETER_PDF_DRIVE_FILE_ID = ""; +const CI_TEST_PROPERTY_NAMES = [ + "OPEN_AI_API_KEY", + "GEMINI_API_KEY", + "VERTEX_AI_PROJECT_ID", + "VERTEX_AI_LOCATION" +]; + +function getRequiredTestScriptProperty(name) { + const value = PropertiesService.getScriptProperties().getProperty(name); + if (!value) { + throw new Error(`Missing required script property: ${name}`); + } + return value; +} + +function setApiKeyAuthForTests() { + GenAIApp.setGeminiAPIKey(getRequiredTestScriptProperty("GEMINI_API_KEY")); + GenAIApp.setOpenAIAPIKey(getRequiredTestScriptProperty("OPEN_AI_API_KEY")); +} + +function setOpenAIAuthForTests() { + GenAIApp.setOpenAIAPIKey(getRequiredTestScriptProperty("OPEN_AI_API_KEY")); +} + +function setVertexAIAuthForTests() { + GenAIApp.setGeminiAuth( + getRequiredTestScriptProperty("VERTEX_AI_PROJECT_ID"), + getRequiredTestScriptProperty("VERTEX_AI_LOCATION") + ); +} + +function setCITestScriptProperties(properties) { + if (!properties || typeof properties !== "object" || Array.isArray(properties)) { + throw new Error("CI test properties must be provided as an object."); + } + + const scriptProperties = {}; + CI_TEST_PROPERTY_NAMES.forEach(name => { + if (Object.prototype.hasOwnProperty.call(properties, name) && properties[name]) { + scriptProperties[name] = String(properties[name]); + } + }); + + const propertyNames = Object.keys(scriptProperties); + if (propertyNames.length === 0) { + throw new Error("No recognized CI test properties were provided."); + } + + const propertyStore = PropertiesService.getScriptProperties(); + CI_TEST_PROPERTY_NAMES.forEach(name => propertyStore.deleteProperty(name)); + propertyStore.setProperties(scriptProperties, false); + return propertyNames; +} + +function clearCITestScriptProperties(propertyNames) { + if (!Array.isArray(propertyNames)) { + throw new Error("CI test property names must be provided as an array."); + } + + const scriptProperties = PropertiesService.getScriptProperties(); + const clearedNames = []; + propertyNames.forEach(name => { + if (CI_TEST_PROPERTY_NAMES.indexOf(name) !== -1) { + scriptProperties.deleteProperty(name); + clearedNames.push(name); + } + }); + return clearedNames; +} // Run all tests function testAll() { @@ -27,9 +96,7 @@ function testAll() { // Helper to set API keys and run tests across models function runTestAcrossModels(testName, setupFunction, runOptions = {}) { - // Set API keys once per batch - GenAIApp.setGeminiAPIKey(GEMINI_API_KEY); - GenAIApp.setOpenAIAPIKey(OPEN_AI_API_KEY); + setApiKeyAuthForTests(); const models = [ { name: GPT_MODEL, label: "GPT" }, @@ -55,6 +122,17 @@ function testSimpleChatInstance() { }, { max_tokens: 1000 }); } +function testVertexAISimpleChat() { + setVertexAIAuthForTests(); + const chat = GenAIApp.newChat(); + chat.addMessage("Reply with exactly: Vertex AI test passed"); + const response = chat.run({ model: GEMINI_MODEL, max_tokens: 64 }); + if (String(response).trim() !== "Vertex AI test passed") { + throw new Error(`Unexpected Vertex AI response: ${response}`); + } + console.log(`Vertex AI simple chat response:\n${response}`); +} + function testFunctionCalling() { const weatherFunction = GenAIApp.newFunction() .setName("getWeather") @@ -135,7 +213,7 @@ function testMaximumAPICalls() { function testInputTokenWarning() { - GenAIApp.setOpenAIAPIKey(OPEN_AI_API_KEY); + setOpenAIAuthForTests(); // Case 1: low threshold should log warning (manual log inspection). const lowThresholdChat = GenAIApp.newChat(); @@ -159,7 +237,7 @@ ${highThresholdResponse}`); } function testCodeInterpreterExcel(driveFileId) { - GenAIApp.setOpenAIAPIKey(OPEN_AI_API_KEY); + setOpenAIAuthForTests(); const inputBlob = DriveApp.getFileById(driveFileId).getBlob(); const chat = GenAIApp.newChat(); chat @@ -172,7 +250,7 @@ function testCodeInterpreterExcel(driveFileId) { } function testCodeInterpreterPDF(driveFileId) { - GenAIApp.setOpenAIAPIKey(OPEN_AI_API_KEY); + setOpenAIAuthForTests(); const inputBlob = DriveApp.getFileById(driveFileId).getBlob(); const chat = GenAIApp.newChat(); chat From 028e479106f708efff4391659068224889d5610c Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Tue, 9 Jun 2026 15:26:50 +0000 Subject: [PATCH 09/12] fix: apply CodeRabbit auto-fixes Fixed 1 file(s) based on 3 unresolved review comments. Co-authored-by: CodeRabbit --- src/testFunctions.gs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/testFunctions.gs b/src/testFunctions.gs index 4ac5c20..f20b310 100644 --- a/src/testFunctions.gs +++ b/src/testFunctions.gs @@ -127,8 +127,8 @@ function testVertexAISimpleChat() { const chat = GenAIApp.newChat(); chat.addMessage("Reply with exactly: Vertex AI test passed"); const response = chat.run({ model: GEMINI_MODEL, max_tokens: 64 }); - if (String(response).trim() !== "Vertex AI test passed") { - throw new Error(`Unexpected Vertex AI response: ${response}`); + if (!String(response).includes("Vertex AI test passed")) { + throw new Error(`Unexpected Vertex AI response - expected to include "Vertex AI test passed" but got: ${response}`); } console.log(`Vertex AI simple chat response:\n${response}`); } From 63452f97b5c0b6b939b809d182f2a46f54b0df52 Mon Sep 17 00:00:00 2001 From: Paul Aubry Date: Tue, 9 Jun 2026 17:38:39 +0200 Subject: [PATCH 10/12] fix test.yml --- .github/workflows/test.yml | 57 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 55 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index cb0169c..48bbdb0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -81,7 +81,7 @@ jobs: needs: syntax-validation concurrency: group: ${{ github.repository }}-${{ github.workflow }}-apps-script-tests - cancel-in-progress: true + cancel-in-progress: false outputs: clasp-available: ${{ steps.clasp-setup.outputs.available }} clasp-authenticated: ${{ steps.clasp-auth.outputs.authenticated }} @@ -243,6 +243,36 @@ jobs: - name: Push clean source to Apps Script if: env.AUTH_TESTS_REQUESTED == 'true' && steps.clasp-auth.outputs.authenticated == 'true' run: clasp push --force + + - name: Backup existing script properties + if: env.AUTH_TESTS_REQUESTED == 'true' && steps.clasp-auth.outputs.authenticated == 'true' + shell: bash + run: | + set -euo pipefail + + # Read existing CI test property values and save them for restoration + backup="$(clasp run 'function() { + var props = PropertiesService.getScriptProperties(); + var backup = {}; + + [ + "OPEN_AI_API_KEY", + "GEMINI_API_KEY", + "VERTEX_AI_PROJECT_ID", + "VERTEX_AI_LOCATION" + ].forEach(function(key) { + var value = props.getProperty(key); + + if (value !== null) { + backup[key] = value; + } + }); + + return JSON.stringify(backup); + }' 2>&1 | tail -n 1 || echo '{}')" + + echo "PROPERTY_BACKUP=$backup" >> "$GITHUB_ENV" + echo "Backed up existing properties: $backup" - name: Provision test script properties if: env.AUTH_TESTS_REQUESTED == 'true' && steps.clasp-auth.outputs.authenticated == 'true' @@ -295,11 +325,12 @@ jobs: clasp run "$function_name" done - - name: Clear test script properties + - name: Restore backed-up script properties if: always() && env.AUTH_TESTS_REQUESTED == 'true' && steps.clasp-auth.outputs.authenticated == 'true' shell: bash run: | set -euo pipefail + # First clear all CI test properties params="$(node -e ' const propertyNames = [ "OPEN_AI_API_KEY", @@ -310,3 +341,25 @@ jobs: process.stdout.write(JSON.stringify([propertyNames])); ')" clasp run clearCITestScriptProperties --params "$params" + + # Then restore any backed-up values that existed before the test run + backup="${PROPERTY_BACKUP:-{}}" + + if [ "$backup" != "{}" ] && [ -n "$backup" ]; then + echo "Restoring backed-up properties: $backup" + + clasp run 'function(backup) { + var props = PropertiesService.getScriptProperties(); + var restored = JSON.parse(backup); + + for (var key in restored) { + if (restored.hasOwnProperty(key)) { + props.setProperty(key, restored[key]); + } + } + + return "Restored " + Object.keys(restored).length + " properties"; + }' --params "$(node -e 'process.stdout.write(JSON.stringify([process.env.PROPERTY_BACKUP || "{}"]))')" + else + echo "No backed-up properties to restore." + fi From ebbe5b48dd936ecd7305c68f3f1702763689310c Mon Sep 17 00:00:00 2001 From: Paul Aubry Date: Tue, 9 Jun 2026 18:18:50 +0200 Subject: [PATCH 11/12] fix --- .claspignore | 2 +- .github/workflows/test.yml | 89 ++++++++++++++++++++++---------------- .gitignore | 1 + README.md | 2 +- appsscript.json | 2 +- src/testFunctions.gs | 40 ++++++++++++++++- 6 files changed, 94 insertions(+), 42 deletions(-) diff --git a/.claspignore b/.claspignore index 51dee08..b5cd294 100644 --- a/.claspignore +++ b/.claspignore @@ -8,7 +8,7 @@ !src/ !src/** -# Documentation files. +# Documentation files — intentionally ignored; do not deploy to Apps Script README.md LICENSE diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 48bbdb0..675d25a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -28,6 +28,10 @@ on: permissions: contents: read +concurrency: + group: ${{ github.repository }}-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false + env: ENABLE_API_KEY_TESTS: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.run_remote_tests == 'true' && inputs['enable-api-key-tests'] || false }} ENABLE_VERTEX_AI_TESTS: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.run_remote_tests == 'true' && inputs['enable-vertex-ai-tests'] || false }} @@ -43,12 +47,12 @@ jobs: steps: - name: Check out repository - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # actions/checkout@v4 with: persist-credentials: false - name: Set up Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # actions/setup-node@v4 with: node-version: "20" @@ -92,20 +96,22 @@ jobs: HAS_OPEN_AI_API_KEY: ${{ secrets.OPEN_AI_API_KEY != '' }} HAS_GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY != '' }} HAS_VERTEX_AI_PROJECT_ID: ${{ secrets.VERTEX_AI_PROJECT_ID != '' }} + HAS_VERTEX_AI_SERVICE_ACCOUNT_JSON: ${{ secrets.VERTEX_AI_SERVICE_ACCOUNT_JSON != '' }} CLASPRC_JSON: ${{ secrets.CLASPRC_JSON }} SCRIPT_ID: ${{ secrets.SCRIPT_ID }} OPEN_AI_API_KEY: ${{ secrets.OPEN_AI_API_KEY }} GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} VERTEX_AI_PROJECT_ID: ${{ secrets.VERTEX_AI_PROJECT_ID }} + VERTEX_AI_SERVICE_ACCOUNT_JSON: ${{ secrets.VERTEX_AI_SERVICE_ACCOUNT_JSON }} steps: - name: Check out repository - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # actions/checkout@v4 with: persist-credentials: false - name: Set up Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # actions/setup-node@v4 with: node-version: "20" @@ -113,7 +119,7 @@ jobs: shell: bash run: | set -euo pipefail - for value in "$CLASPRC_JSON" "$SCRIPT_ID" "$OPEN_AI_API_KEY" "$GEMINI_API_KEY" "$VERTEX_AI_PROJECT_ID"; do + for value in "$CLASPRC_JSON" "$SCRIPT_ID" "$OPEN_AI_API_KEY" "$GEMINI_API_KEY" "$VERTEX_AI_PROJECT_ID" "$VERTEX_AI_SERVICE_ACCOUNT_JSON"; do if [ -n "$value" ]; then echo "::add-mask::$value" fi @@ -142,7 +148,7 @@ jobs: echo "API key test secrets are configured." - - name: Validate Vertex AI test secrets + - name: Validate Vertex AI secrets if: env.ENABLE_VERTEX_AI_TESTS == 'true' shell: bash run: | @@ -154,12 +160,18 @@ jobs: if [ -z "$VERTEX_AI_LOCATION" ]; then missing+=("VERTEX_AI_LOCATION repository variable or secret") fi + if [ "$HAS_VERTEX_AI_SERVICE_ACCOUNT_JSON" != "true" ]; then + missing+=("VERTEX_AI_SERVICE_ACCOUNT_JSON") + fi if [ "${#missing[@]}" -gt 0 ]; then printf '::error::Vertex AI tests were requested but required configuration value(s) are missing: %s\n' "${missing[*]}" exit 1 fi + printf '%s' "$VERTEX_AI_SERVICE_ACCOUNT_JSON" > "$GITHUB_WORKSPACE/vertex-ai-service-account.json" + node -e 'const fs = require("fs"); const p = process.env.GITHUB_WORKSPACE + "/vertex-ai-service-account.json"; const text = fs.readFileSync(p, "utf8"); JSON.parse(text); console.log("Vertex AI service account JSON is valid and written to", p);' + echo "Vertex AI test configuration is present." - name: Prepare clasp project and credentials @@ -250,29 +262,28 @@ jobs: run: | set -euo pipefail - # Read existing CI test property values and save them for restoration - backup="$(clasp run 'function() { - var props = PropertiesService.getScriptProperties(); - var backup = {}; - - [ - "OPEN_AI_API_KEY", - "GEMINI_API_KEY", - "VERTEX_AI_PROJECT_ID", - "VERTEX_AI_LOCATION" - ].forEach(function(key) { - var value = props.getProperty(key); + output="$(clasp run backupCITestScriptProperties 2>&1 | tail -n 1)" + status=$? - if (value !== null) { - backup[key] = value; - } - }); + if [ "$status" -ne 0 ]; then + echo "::error::Failed to backup existing CI test properties. clasp run exit status: $status. Output: $output" + echo "PROPERTY_BACKUP_FAILED=true" >> "$GITHUB_ENV" + echo "PROPERTY_BACKUP={}" >> "$GITHUB_ENV" + exit 1 + fi - return JSON.stringify(backup); - }' 2>&1 | tail -n 1 || echo '{}')" + if [ -z "$output" ]; then + echo "::error::Backup command returned empty output. PROPERTY_BACKUP cannot be determined." + echo "PROPERTY_BACKUP_FAILED=true" >> "$GITHUB_ENV" + echo "PROPERTY_BACKUP={}" >> "$GITHUB_ENV" + exit 1 + fi - echo "PROPERTY_BACKUP=$backup" >> "$GITHUB_ENV" - echo "Backed up existing properties: $backup" + echo "PROPERTY_BACKUP=$output" >> "$GITHUB_ENV" + if [ "$output" = "{}" ]; then + echo "No existing CI test properties were found; PROPERTY_BACKUP is empty JSON." + fi + echo "Backed up existing properties: $output" - name: Provision test script properties if: env.AUTH_TESTS_REQUESTED == 'true' && steps.clasp-auth.outputs.authenticated == 'true' @@ -325,6 +336,19 @@ jobs: clasp run "$function_name" done + - name: Cleanup Vertex AI service account file + if: always() && env.ENABLE_VERTEX_AI_TESTS == 'true' + shell: bash + run: | + set -euo pipefail + file="$GITHUB_WORKSPACE/vertex-ai-service-account.json" + if [ -f "$file" ]; then + rm -f "$file" + echo "Removed Vertex AI service account file: $file" + else + echo "No vertex-ai-service-account.json file present." + fi + - name: Restore backed-up script properties if: always() && env.AUTH_TESTS_REQUESTED == 'true' && steps.clasp-auth.outputs.authenticated == 'true' shell: bash @@ -348,18 +372,7 @@ jobs: if [ "$backup" != "{}" ] && [ -n "$backup" ]; then echo "Restoring backed-up properties: $backup" - clasp run 'function(backup) { - var props = PropertiesService.getScriptProperties(); - var restored = JSON.parse(backup); - - for (var key in restored) { - if (restored.hasOwnProperty(key)) { - props.setProperty(key, restored[key]); - } - } - - return "Restored " + Object.keys(restored).length + " properties"; - }' --params "$(node -e 'process.stdout.write(JSON.stringify([process.env.PROPERTY_BACKUP || "{}"]))')" + clasp run restoreCITestScriptProperties --params "$(node -e 'process.stdout.write(JSON.stringify([process.env.PROPERTY_BACKUP || "{}"]))')" else echo "No backed-up properties to restore." fi diff --git a/.gitignore b/.gitignore index 09c562b..10c72b6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # Local clasp configuration generated from the SCRIPT_ID secret in CI. .clasp.json +vertex-ai-service-account.json # Local dependencies and tooling artifacts node_modules/ diff --git a/README.md b/README.md index c9f76ed..f0f2b3f 100644 --- a/README.md +++ b/README.md @@ -114,7 +114,7 @@ Configure secrets in **GitHub repository → Settings → Secrets and variables | `OPEN_AI_API_KEY` | `enable-api-key-tests=true` | Create/copy an OpenAI API key from your OpenAI account and store only the key value as the secret. | | `GEMINI_API_KEY` | `enable-api-key-tests=true` | Create/copy a Gemini API key from Google AI Studio or the Google Cloud API credentials page and store only the key value as the secret. | | `VERTEX_AI_PROJECT_ID` | `enable-vertex-ai-tests=true` | Use the Google Cloud project ID for the standard GCP project linked to the Apps Script project and with Vertex AI enabled. | -| `VERTEX_AI_LOCATION` | `enable-vertex-ai-tests=true` unless you accept the workflow default of `global` | Configure it as either a repository variable or secret. Use the Vertex AI location you want the tests to call, such as `global` or `us-central1`. | +| `VERTEX_AI_LOCATION` | `enable-vertex-ai-tests=true` | Optional; defaults to `global` if not provided. Configure it as either a repository variable or secret. Use the Vertex AI location you want the tests to call, such as `global` or `us-central1`. | | `VERTEX_AI_SERVICE_ACCOUNT_JSON` | `enable-vertex-ai-tests=true` | Create a Google Cloud service account with the permissions your Vertex AI test flow requires, create a JSON key if your organization allows key-based CI credentials, and store the complete JSON document as the secret. | > **Note:** The current Apps Script Vertex AI smoke test authenticates through the linked Apps Script/GCP project via `ScriptApp.getOAuthToken()` after CI pushes the code. The workflow still validates `VERTEX_AI_SERVICE_ACCOUNT_JSON` when Vertex AI tests are requested so the repository has an explicit place for future Vertex AI CI credential needs. diff --git a/appsscript.json b/appsscript.json index 18adf5c..5925d34 100644 --- a/appsscript.json +++ b/appsscript.json @@ -5,7 +5,7 @@ "access": "MYSELF" }, "oauthScopes": [ - "https://www.googleapis.com/auth/cloud-platform", + "https://www.googleapis.com/auth/aiplatform", "https://www.googleapis.com/auth/drive", "https://www.googleapis.com/auth/script.external_request", "https://www.googleapis.com/auth/script.storage" diff --git a/src/testFunctions.gs b/src/testFunctions.gs index f20b310..957fd98 100644 --- a/src/testFunctions.gs +++ b/src/testFunctions.gs @@ -57,6 +57,20 @@ function setCITestScriptProperties(properties) { return propertyNames; } +function backupCITestScriptProperties() { + const props = PropertiesService.getScriptProperties(); + const backup = {}; + + CI_TEST_PROPERTY_NAMES.forEach(name => { + const value = props.getProperty(name); + if (value !== null) { + backup[name] = value; + } + }); + + return JSON.stringify(backup); +} + function clearCITestScriptProperties(propertyNames) { if (!Array.isArray(propertyNames)) { throw new Error("CI test property names must be provided as an array."); @@ -65,7 +79,7 @@ function clearCITestScriptProperties(propertyNames) { const scriptProperties = PropertiesService.getScriptProperties(); const clearedNames = []; propertyNames.forEach(name => { - if (CI_TEST_PROPERTY_NAMES.indexOf(name) !== -1) { + if (CI_TEST_PROPERTY_NAMES.includes(name)) { scriptProperties.deleteProperty(name); clearedNames.push(name); } @@ -73,6 +87,30 @@ function clearCITestScriptProperties(propertyNames) { return clearedNames; } +function restoreCITestScriptProperties(backup) { + if (typeof backup !== 'string') { + throw new Error('Backup payload must be a JSON string.'); + } + + let restored; + try { + restored = JSON.parse(backup); + } catch (e) { + throw new Error('Failed to parse backup JSON: ' + e.message); + } + + const props = PropertiesService.getScriptProperties(); + const restoredKeys = []; + Object.keys(restored).forEach(key => { + if (CI_TEST_PROPERTY_NAMES.includes(key)) { + props.setProperty(key, String(restored[key])); + restoredKeys.push(key); + } + }); + + return 'Restored ' + restoredKeys.length + ' properties'; +} + // Run all tests function testAll() { testSimpleChatInstance(); From 5135182c52538763ff837a4df96bfb0f7523e5a3 Mon Sep 17 00:00:00 2001 From: Paul Aubry Date: Tue, 9 Jun 2026 18:44:41 +0200 Subject: [PATCH 12/12] temp modif for test --- .github/workflows/test.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 675d25a..28e8588 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -33,9 +33,9 @@ concurrency: cancel-in-progress: false env: - ENABLE_API_KEY_TESTS: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.run_remote_tests == 'true' && inputs['enable-api-key-tests'] || false }} - ENABLE_VERTEX_AI_TESTS: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.run_remote_tests == 'true' && inputs['enable-vertex-ai-tests'] || false }} - AUTH_TESTS_REQUESTED: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.run_remote_tests == 'true' && (inputs['enable-api-key-tests'] || inputs['enable-vertex-ai-tests']) || false }} + ENABLE_API_KEY_TESTS: true + ENABLE_VERTEX_AI_TESTS: false + AUTH_TESTS_REQUESTED: true API_KEY_TEST_FUNCTIONS: ${{ vars.API_KEY_TEST_FUNCTIONS || 'testSimpleChatInstance' }} VERTEX_AI_TEST_FUNCTIONS: ${{ vars.VERTEX_AI_TEST_FUNCTIONS || 'testVertexAISimpleChat' }} VERTEX_AI_LOCATION: ${{ vars.VERTEX_AI_LOCATION || secrets.VERTEX_AI_LOCATION || 'global' }} @@ -80,7 +80,7 @@ jobs: apps-script-tests: name: Apps Script remote tests - if: github.event_name == 'workflow_dispatch' && github.event.inputs.run_remote_tests == 'true' + if: github.event_name == 'pull_request' runs-on: ubuntu-latest needs: syntax-validation concurrency: