Skip to content

Instantly share code, notes, and snippets.

@DuaneNielsen
Created June 23, 2026 16:08
Show Gist options
  • Select an option

  • Save DuaneNielsen/70fd0618fa7f8a88d130296cea1d5cc9 to your computer and use it in GitHub Desktop.

Select an option

Save DuaneNielsen/70fd0618fa7f8a88d130296cea1d5cc9 to your computer and use it in GitHub Desktop.
Apps Script Web App Setup

Apps Script Web App Setup

A static-ish website hosted on Google Apps Script, accessible via a stable HTTPS URL under your domain.

Constraints (Broadcom / Google Workspace)

  • ANYONE_ANONYMOUS / ANYONE access blocked by domain policy — use DOMAIN
  • Public Drive file sharing blocked — embed images as base64 data URIs
  • No Google Sites write API — Apps Script web apps are the workaround

Minimal file structure

appsscript.json   # manifest
Code.js           # doGet() entry point
index.html        # your HTML

appsscript.json

{
  "timeZone": "America/New_York",
  "dependencies": {},
  "exceptionLogging": "STACKDRIVER",
  "runtimeVersion": "V8",
  "webapp": {
    "executeAs": "USER_DEPLOYING",
    "access": "DOMAIN"
  }
}

Code.js

function doGet() {
  return HtmlService.createHtmlOutputFromFile('index')
    .setTitle("Page Title")
    .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
}

Deploying via Python (stable URL)

Push files → create version → update the same deployment in place (URL never changes).

import json
from google.oauth2.credentials import Credentials
from googleapiclient.discovery import build

SCRIPT_ID    = "<your-script-id>"
DEPLOY_ID    = "<your-deployment-id>"   # get this after first manual deploy

with open("/home/duane/.config/gcloud/application_default_credentials.json") as f:
    data = json.load(f)

creds = Credentials(
    token=None,
    refresh_token=data["refresh_token"],
    token_uri="https://oauth2.googleapis.com/token",
    client_id=data["client_id"],
    client_secret=data["client_secret"],
    quota_project_id="agentic-sre-demo",   # personal GCP project with Drive API enabled
)

svc = build("script", "v1", credentials=creds)

# 1. Push updated source files
svc.projects().updateContent(
    scriptId=SCRIPT_ID,
    body={"files": [
        {"name": "appsscript", "type": "JSON",           "source": open("appsscript.json").read()},
        {"name": "Code",       "type": "SERVER_JS",      "source": open("Code.js").read()},
        {"name": "index",      "type": "HTML",           "source": open("index.html").read()},
    ]}
).execute()

# 2. Create a new version
ver = svc.projects().versions().create(
    scriptId=SCRIPT_ID, body={"description": "v2"}
).execute()

# 3. Update canonical deployment in place
svc.projects().deployments().update(
    scriptId=SCRIPT_ID,
    deploymentId=DEPLOY_ID,
    body={"deploymentConfig": {
        "scriptId": SCRIPT_ID,
        "versionNumber": ver["versionNumber"],
        "manifestFileName": "appsscript",
        "description": "latest",
    }}
).execute()

print("Done — URL unchanged")

GCP auth setup (one-time)

# Authenticate as Broadcom account
gcloud auth application-default login \
  --scopes="openid,https://www.googleapis.com/auth/userinfo.email,\
https://www.googleapis.com/auth/cloud-platform,\
https://www.googleapis.com/auth/drive,\
https://www.googleapis.com/auth/script.projects,\
https://www.googleapis.com/auth/script.deployments" \
  --no-launch-browser

# Enable Drive API on personal project (needed for quota)
gcloud services enable drive.googleapis.com --project=agentic-sre-demo \
  --account=duane.nielsen.rocks@gmail.com

# Grant Broadcom account quota rights on personal project
gcloud projects add-iam-policy-binding agentic-sre-demo \
  --member="user:duane.nielsen@broadcom.com" \
  --role="roles/serviceusage.serviceUsageConsumer" \
  --account=duane.nielsen.rocks@gmail.com

Credentials land at ~/.config/gcloud/application_default_credentials.json.

First deploy (manual, one-time)

  1. Open script.google.com, create a new project
  2. Deploy → New deployment → Web app → access = Domain
  3. Copy the Script ID (Project Settings) and Deployment ID — paste into your push script

After that, all updates go through the Python script above; the URL stays stable.

Screenshotting (Playwright + CDP)

A pre-authed Chrome profile lives at ~/duane_ai_site/pw-user-data/.

import subprocess, os, time
from playwright.sync_api import sync_playwright

env = {**os.environ, "DISPLAY": ":1", "WAYLAND_DISPLAY": "wayland-1"}
proc = subprocess.Popen([
    "google-chrome", "--remote-debugging-port=9222",
    "--user-data-dir=/home/duane/duane_ai_site/pw-user-data",
    "--no-first-run", "--no-default-browser-check",
    "--password-store=basic", "--use-mock-keychain",
    "--no-sandbox", "--window-size=1440,900",
], env=env, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
time.sleep(2)

with sync_playwright() as p:
    browser = p.chromium.connect_over_cdp("http://localhost:9222")
    page = browser.contexts[0].pages[0]
    page.goto("<your-app-url>")
    page.wait_for_load_state("networkidle")
    page.screenshot(path="/tmp/site.png")

proc.terminate()

Do NOT use --headless=new — Drive iframes need a real display. Kill any existing Chrome on the same profile first: pkill -f duane_ai_site/pw-user-data.

Re-auth if session expires:

python3 ~/duane_ai_site/auth.py           # sign in via Broadcom SSO
python3 ~/duane_ai_site/auth.py --finish  # save and close
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment