mikeboone Claude Opus 5 commited on
Commit
ae9ba39
Β·
1 Parent(s): 731af14

chore(scripts): add scripts/ for operational tooling

Browse files

Moves whats_deployed.py and setup_test_mcp_secrets.py out of scratch/ (which
is gitignored and defined as disposable) into a tracked scripts/ dir. Both are
standing operational tools, not throwaways β€” whats_deployed.py is meant to run
at the start of every session.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

scripts/setup_test_mcp_secrets.py ADDED
@@ -0,0 +1,154 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ """
3
+ One-off: copy the MCP Space secrets from the local .env into the TEST MCP Space.
4
+
5
+ source ./demoprep/bin/activate && python scratch/setup_test_mcp_secrets.py
6
+
7
+ Reads values straight from .env and pushes them to
8
+ `thoughtspot-demoprep/test-mcp` as Repository Secrets. Secret VALUES are never
9
+ printed β€” only the key names and whether each was set. HF's API is write-only
10
+ for secrets, so values cannot be read back out of the prod Space; .env is the
11
+ source of truth.
12
+
13
+ Why a separate MCP test Space exists: on 2026-08-25 a logging change went
14
+ straight to the prod MCP Space and failed every build (PermissionError on
15
+ /app/logs under the Space's non-root user). Deploy to test first.
16
+
17
+ Flags:
18
+ --dry-run show what would be set, touch nothing
19
+ --new-token generate a FRESH MCP_ACCESS_TOKEN for test instead of
20
+ reusing prod's (recommended β€” a leaked test token should
21
+ not unlock the prod MCP Space). Prints it ONCE so you can
22
+ hand it to the calling agent; it is not stored locally.
23
+ """
24
+
25
+ import argparse
26
+ import secrets
27
+ import sys
28
+ from pathlib import Path
29
+
30
+ REPO_ID = "thoughtspot-demoprep/test-mcp"
31
+ ENV_PATH = Path(__file__).resolve().parent.parent / ".env"
32
+
33
+ # Required by the MCP Space (see README.md "Required secrets").
34
+ REQUIRED = [
35
+ "SUPABASE_URL",
36
+ "SUPABASE_ANON_KEY",
37
+ "OPENAI_API_KEY",
38
+ "MCP_OWNER_EMAIL",
39
+ "MCP_TS_ENV_LABEL",
40
+ "MCP_ACCESS_TOKEN",
41
+ ]
42
+ # Optional but commonly set.
43
+ OPTIONAL = ["GOOGLE_API_KEY", "ANTHROPIC_API_KEY", "MCP_SHARE_WITH",
44
+ "MCP_MAX_CONCURRENT_BUILDS"]
45
+
46
+
47
+ def load_env(path: Path) -> dict:
48
+ if not path.exists():
49
+ sys.exit(f"ERROR: {path} not found β€” run from the repo root.")
50
+ env = {}
51
+ for raw in path.read_text().splitlines():
52
+ line = raw.strip()
53
+ if not line or line.startswith("#") or "=" not in line:
54
+ continue
55
+ k, v = line.split("=", 1)
56
+ env[k.strip()] = v.strip().strip('"').strip("'")
57
+ return env
58
+
59
+
60
+ def main() -> None:
61
+ ap = argparse.ArgumentParser()
62
+ ap.add_argument("--dry-run", action="store_true")
63
+ ap.add_argument("--new-token", action="store_true")
64
+ args = ap.parse_args()
65
+
66
+ env = load_env(ENV_PATH)
67
+
68
+ # Collect every TS_ENV_<n>_* triple present, plus resolve the KEY_VAR
69
+ # indirection: TS_ENV_<n>_KEY_VAR holds the NAME of the var carrying the
70
+ # actual trusted-auth key, so that var has to be copied too.
71
+ ts_keys = sorted(k for k in env if k.startswith("TS_ENV_"))
72
+ indirect = []
73
+ for k in ts_keys:
74
+ if k.endswith("_KEY_VAR"):
75
+ target = env.get(k, "").strip()
76
+ if target and target in env:
77
+ indirect.append(target)
78
+ elif target:
79
+ print(f" !! {k} points at '{target}' which is NOT in .env "
80
+ f"β€” trusted auth will fail on test until you set it.")
81
+
82
+ to_set = REQUIRED + [k for k in OPTIONAL if k in env] + ts_keys + indirect
83
+ # de-dupe, preserve order
84
+ seen, ordered = set(), []
85
+ for k in to_set:
86
+ if k not in seen:
87
+ seen.add(k)
88
+ ordered.append(k)
89
+
90
+ fresh_token = None
91
+ if args.new_token:
92
+ fresh_token = secrets.token_urlsafe(32)
93
+
94
+ missing = [k for k in ordered
95
+ if not env.get(k) and not (k == "MCP_ACCESS_TOKEN" and fresh_token)]
96
+
97
+ print(f"Target Space: {REPO_ID}")
98
+ print(f"Reading: {ENV_PATH}")
99
+ print(f"\n{len(ordered)} secret(s) to set:")
100
+ for k in ordered:
101
+ if k == "MCP_ACCESS_TOKEN" and fresh_token:
102
+ state = "NEWLY GENERATED"
103
+ elif env.get(k):
104
+ state = "ok"
105
+ else:
106
+ state = "MISSING from .env"
107
+ print(f" {k:<28} {state}")
108
+
109
+ if missing:
110
+ print(f"\nWARNING: {len(missing)} required value(s) missing from .env: "
111
+ f"{', '.join(missing)}")
112
+ print("The Space will boot but builds will fail until these are set.")
113
+
114
+ if args.dry_run:
115
+ print("\n--dry-run: nothing was changed.")
116
+ return
117
+
118
+ if input("\nPush these to the test Space? [y/N] ").strip().lower() != "y":
119
+ sys.exit("Aborted.")
120
+
121
+ from huggingface_hub import HfApi
122
+
123
+ hf_token = env.get("HF_TOKEN")
124
+ if not hf_token:
125
+ sys.exit("ERROR: HF_TOKEN not in .env")
126
+ api = HfApi(token=hf_token)
127
+
128
+ ok, failed = 0, []
129
+ for k in ordered:
130
+ value = fresh_token if (k == "MCP_ACCESS_TOKEN" and fresh_token) else env.get(k)
131
+ if not value:
132
+ continue
133
+ try:
134
+ api.add_space_secret(repo_id=REPO_ID, key=k, value=value)
135
+ print(f" set {k}")
136
+ ok += 1
137
+ except Exception as exc:
138
+ print(f" FAILED {k}: {type(exc).__name__}: {exc}")
139
+ failed.append(k)
140
+
141
+ print(f"\nDone: {ok} set, {len(failed)} failed.")
142
+ if fresh_token:
143
+ print("\n" + "=" * 62)
144
+ print("FRESH TEST MCP_ACCESS_TOKEN (shown once β€” copy it now):")
145
+ print(f" {fresh_token}")
146
+ print("Give this to the calling agent for TEST. Prod keeps its own token.")
147
+ print("=" * 62)
148
+ print("\nNEXT: confirm the TS target. If MCP_TS_ENV_LABEL / MCP_OWNER_EMAIL")
149
+ print("match prod, test builds will create objects in the PROD ThoughtSpot")
150
+ print("instance. Point test at a non-prod TS env or a throwaway owner.")
151
+
152
+
153
+ if __name__ == "__main__":
154
+ main()
scripts/whats_deployed.py ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ """
3
+ What is actually deployed, right now, on every Space.
4
+
5
+ .venv/bin/python scratch/whats_deployed.py
6
+
7
+ Answers the question that was unanswerable during the 2026-08-25 MCP outage:
8
+ which commit is each Space really running, and does any local branch have it?
9
+
10
+ For each Space it prints the live commit, the runtime stage, and β€” the part that
11
+ matters β€” a DRIFT verdict:
12
+
13
+ in sync the deployed sha is the tip of its source branch
14
+ behind source branch has newer commits not deployed yet
15
+ DRIFT the deployed sha is on NO local branch. Something was pushed
16
+ straight to the Space. The next normal deploy will revert it.
17
+
18
+ DRIFT is the condition that caused the outage's second-order problem: hotfix
19
+ 4b87c36 lived only on the prod Space for ~10 hours while every local branch
20
+ still carried the bug.
21
+
22
+ Exit code is 1 if any Space is in DRIFT or not RUNNING, so this can gate a
23
+ deploy script later.
24
+ """
25
+
26
+ import subprocess
27
+ import time
28
+ import sys
29
+
30
+ # Space -> the branch it is supposed to be deployed from.
31
+ SPACES = [
32
+ ("thoughtspot-demoprep/mcp", "deploy/mcp", "MCP PROD"),
33
+ ("thoughtspot-demoprep/test-mcp", "deploy/mcp", "MCP test"),
34
+ ("thoughtspot-dp/demoprep", "origin/main", "app PROD"),
35
+ ("thoughtspot-dp/test-demoprep", "spike/portal-template", "app test"),
36
+ ]
37
+
38
+
39
+ def git(*args, _tries: int = 5) -> str:
40
+ """Run git, retrying if the host is temporarily out of process slots.
41
+
42
+ macOS fork() raises BlockingIOError when the per-user process table is
43
+ full (e.g. thousands of zombie browser helpers). That is a host condition,
44
+ not a git failure β€” back off and retry rather than crashing the report.
45
+ """
46
+ for attempt in range(_tries):
47
+ try:
48
+ return subprocess.run(("git",) + args, capture_output=True,
49
+ text=True).stdout.strip()
50
+ except BlockingIOError:
51
+ if attempt == _tries - 1:
52
+ raise
53
+ time.sleep(0.5 * (attempt + 1))
54
+ return ""
55
+
56
+
57
+ def hf_token() -> str:
58
+ for line in open(".env"):
59
+ line = line.strip()
60
+ if line.startswith("HF_TOKEN="):
61
+ return line.split("=", 1)[1].strip().strip('"').strip("'")
62
+ sys.exit("ERROR: HF_TOKEN not found in .env")
63
+
64
+
65
+ def main() -> None:
66
+ from huggingface_hub import HfApi
67
+
68
+ api = HfApi(token=hf_token())
69
+ git("fetch", "--all", "--quiet")
70
+
71
+ print(f"{'SPACE':<34} {'TIER':<10} {'DEPLOYED':<9} {'STAGE':<14} SOURCE / DRIFT")
72
+ print("-" * 104)
73
+
74
+ problems = 0
75
+ for repo_id, source, tier in SPACES:
76
+ try:
77
+ info = api.space_info(repo_id)
78
+ except Exception as exc:
79
+ print(f"{repo_id:<34} {tier:<10} {'?':<9} {'UNREACHABLE':<14} {type(exc).__name__}")
80
+ problems += 1
81
+ continue
82
+
83
+ sha = (info.sha or "")[:7]
84
+ stage = getattr(info.runtime, "stage", "?") or "?"
85
+
86
+ # Does any local branch contain the deployed commit?
87
+ if sha and git("cat-file", "-t", sha) == "commit":
88
+ holders = [b.strip().lstrip("* +").strip()
89
+ for b in git("branch", "--contains", sha).splitlines() if b.strip()]
90
+ if not holders:
91
+ verdict = "DRIFT β€” on no local branch! pushed direct to Space"
92
+ problems += 1
93
+ else:
94
+ tip = git("rev-parse", "--short=7", source)
95
+ if tip == sha:
96
+ verdict = f"in sync with {source}"
97
+ else:
98
+ ahead = git("rev-list", "--count", f"{sha}..{source}")
99
+ behind = git("rev-list", "--count", f"{source}..{sha}")
100
+ if behind != "0":
101
+ verdict = (f"DRIFT β€” {behind} commit(s) deployed that {source} "
102
+ f"lacks (+{ahead} undeployed)")
103
+ problems += 1
104
+ else:
105
+ verdict = f"behind {source} by {ahead} commit(s)"
106
+ else:
107
+ verdict = f"deployed sha {sha} UNKNOWN locally β€” run git fetch --all"
108
+ problems += 1
109
+
110
+ if stage != "RUNNING":
111
+ problems += 1
112
+
113
+ print(f"{repo_id:<34} {tier:<10} {sha:<9} {stage:<14} {verdict}")
114
+
115
+ print()
116
+ if problems:
117
+ print(f"{problems} issue(s). A DRIFT verdict means: merge the Space's commit back "
118
+ f"into its source branch BEFORE the next deploy, or it gets reverted.")
119
+ else:
120
+ print("All Spaces running and in sync with their source branches.")
121
+
122
+ print("\nNote: 'RUNNING' only means the container booted. It does NOT mean builds "
123
+ "work β€” GET / returned ok throughout the 2026-08-25 total build outage.")
124
+ sys.exit(1 if problems else 0)
125
+
126
+
127
+ if __name__ == "__main__":
128
+ main()