Skip to content

fix(gappsscript): default update_script_content to safe merge mode - #982

Merged
taylorwilsdon merged 8 commits into
taylorwilsdon:mainfrom
syf2211:fix/update-script-content-merge-default-923
Jul 30, 2026
Merged

fix(gappsscript): default update_script_content to safe merge mode#982
taylorwilsdon merged 8 commits into
taylorwilsdon:mainfrom
syf2211:fix/update-script-content-merge-default-923

Conversation

@syf2211

@syf2211 syf2211 commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Summary

update_script_content now defaults to merge=true, overlaying supplied files onto the current Apps Script project by file name instead of replacing the entire project. This prevents accidental deletion of omitted files when LLM callers perform partial updates.

Motivation

Fixes #923. Google Apps Script projects.updateContent replaces the full project file set, but the tool description implied partial updates. Agents repeatedly deleted unrelated files when pushing only the files they changed.

Changes

  • Add _merge_script_files helper and merge parameter (default true) to update_script_content
  • merge=true: fetch current content, overlay updates by file name, push merged set
  • merge=false: preserve prior full-replacement semantics for intentional deletions
  • Clarify tool docstring and update README / Apps Script reference docs
  • Add regression tests for merge overlay, file creation, and full-replacement mode

Tests

  • uv run pytest tests/gappsscript/test_apps_script_tools.py -q → 26 passed
  • uv run ruff check gappsscript/apps_script_tools.py tests/gappsscript/test_apps_script_tools.py → clean

Notes

  • Behavior change: callers who relied on deletion-by-omission must pass merge=false with the complete desired file set
  • merge=true adds one getContent round-trip before updateContent
  • File deletion is still available via merge=false; no silent delete path in merge mode

Fixes #923

Summary by CodeRabbit

  • New Features

    • Script content updates now merge with existing project files by default.
    • Added an option to replace the entire project, including deleting omitted files.
    • Improved file matching and validation for updates involving duplicate names or file types.
  • Bug Fixes

    • Prevented concurrent updates to the same script from overwriting each other.
  • Documentation

    • Updated tool descriptions, examples, and reference documentation to explain merge and replacement behavior.

Add merge=True default so partial file updates overlay by name instead of
deleting omitted project files. Document destructive full-replacement when
merge=False and update skill/reference docs for LLM callers.

Fixes taylorwilsdon#923
@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

update_script_content now merges files by default, supports explicit full-project replacement, validates merge inputs, serializes concurrent updates per script, and documents the resulting semantics.

Changes

Apps Script content updates

Layer / File(s) Summary
Update contract and merge helpers
gappsscript/apps_script_tools.py, tests/gappsscript/test_apps_script_tools.py
Adds merge=True, normalizes file inputs, overlays files by (name, type), infers unambiguous types, and raises UserInputError for invalid inputs.
Merge and replacement execution
gappsscript/apps_script_tools.py, tests/gappsscript/test_apps_script_tools.py
Serializes per-script updates, fetches existing content for merges, replaces all files when disabled, and tests payloads and concurrent merges.
Tool documentation updates
README.md, gappsscript/README.md, skills/managing-google-workspace/references/apps-script.md, tests/gappsscript/test_apps_script_tools.py
Documents default merge behavior, explicit replacement, and deletion of omitted files.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant update_script_content
  participant AppsScriptAPI
  Caller->>update_script_content: Submit files and merge option
  update_script_content->>AppsScriptAPI: Fetch current files when merge=true
  AppsScriptAPI-->>update_script_content: Return project content
  update_script_content->>AppsScriptAPI: Update merged or replacement file set
  AppsScriptAPI-->>Caller: Return status
Loading

Suggested labels: bug

Suggested reviewers: taylorwilsdon

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 52.38% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly states the main change: defaulting update_script_content to safe merge mode.
Description check ✅ Passed The description is mostly complete and includes summary, motivation, changes, tests, and notes.
Linked Issues check ✅ Passed The PR satisfies #923 by making merge the default, preserving full-replacement with merge=false, and updating docs/tests.
Out of Scope Changes check ✅ Passed The added in-process lock is a supporting correctness change for concurrent updates, not unrelated scope creep.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Fix failing CI checks
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
tests/gappsscript/test_apps_script_tools.py (1)

194-220: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Exercise the default merge contract.

This test passes merge=True, so a future default change would not be caught. Omit the argument here to verify the advertised default behavior.

Proposed test adjustment
     result = await _update_script_content_impl(
         service=mock_service,
         user_google_email="test@example.com",
         script_id="test123",
         files=files_to_update,
-        merge=True,
     )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/gappsscript/test_apps_script_tools.py` around lines 194 - 220, Update
test_update_script_content_merge_fetches_existing_files to omit the explicit
merge=True argument when calling _update_script_content_impl, so the test
exercises the function’s advertised default merge behavior while preserving the
existing setup and assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@tests/gappsscript/test_apps_script_tools.py`:
- Around line 194-220: Update
test_update_script_content_merge_fetches_existing_files to omit the explicit
merge=True argument when calling _update_script_content_impl, so the test
exercises the function’s advertised default merge behavior while preserving the
existing setup and assertions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 00c7d660-e12b-4228-93b5-1894a752e5eb

📥 Commits

Reviewing files that changed from the base of the PR and between d9d09fa and ce3f4c9.

📒 Files selected for processing (6)
  • README.md
  • README_NEW.md
  • gappsscript/README.md
  • gappsscript/apps_script_tools.py
  • skills/managing-google-workspace/references/apps-script.md
  • tests/gappsscript/test_apps_script_tools.py

@taylorwilsdon taylorwilsdon self-assigned this Jul 30, 2026
@taylorwilsdon taylorwilsdon added the enhancement New feature or request label Jul 30, 2026
@taylorwilsdon

Copy link
Copy Markdown
Owner

Thanks!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
gappsscript/apps_script_tools.py (1)

47-50: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reject updates with a missing file name.

Default merge mode silently drops these entries, then pushes the unchanged project and reports success. Raise ValueError instead so callers can correct the malformed file payload.

Proposed fix
-    for file in updated_files:
+    for index, file in enumerate(updated_files):
         name = file.get("name")
         if not name:
-            continue
+            raise ValueError(f"File at index {index} is missing a non-empty 'name'.")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@gappsscript/apps_script_tools.py` around lines 47 - 50, Update the
updated_files processing loop to raise ValueError when an entry lacks a valid
name instead of continuing past it. Preserve normal processing for entries with
names, ensuring malformed file payloads cannot be silently dropped or reported
as a successful unchanged update.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@gappsscript/apps_script_tools.py`:
- Around line 47-50: Update the updated_files processing loop to raise
ValueError when an entry lacks a valid name instead of continuing past it.
Preserve normal processing for entries with names, ensuring malformed file
payloads cannot be silently dropped or reported as a successful unchanged
update.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f2bfddb9-9475-4e2b-b986-5cb23c18ca3b

📥 Commits

Reviewing files that changed from the base of the PR and between ce3f4c9 and 2d882f3.

📒 Files selected for processing (2)
  • gappsscript/apps_script_tools.py
  • tests/gappsscript/test_apps_script_tools.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
gappsscript/apps_script_tools.py (1)

382-394: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Prevent lost updates during merge=True content updates.

updateScriptContent reads the project once, merges updates, and submits the complete files array. If another worker or external change modifies the same script between getContent and updateContent, this update can be built from stale state and overwrite those changes. Add per-script_id serialization/locking, or implement retry/validate-before-write logic that detects concurrent misses.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@gappsscript/apps_script_tools.py` around lines 382 - 394, Update
updateScriptContent’s merge=True path to prevent stale getContent results from
overwriting concurrent changes: serialize operations per script_id with a
per-script lock, or validate the fetched project state immediately before
updateContent and retry when it changed. Ensure concurrent updates for the same
script are detected or ordered safely while preserving the existing merge
behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@gappsscript/apps_script_tools.py`:
- Around line 382-394: Update updateScriptContent’s merge=True path to prevent
stale getContent results from overwriting concurrent changes: serialize
operations per script_id with a per-script lock, or validate the fetched project
state immediately before updateContent and retry when it changed. Ensure
concurrent updates for the same script are detected or ordered safely while
preserving the existing merge behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 120b336c-3242-42cd-bbb3-357ea66b15be

📥 Commits

Reviewing files that changed from the base of the PR and between 2d882f3 and 037851e.

📒 Files selected for processing (2)
  • gappsscript/apps_script_tools.py
  • tests/gappsscript/test_apps_script_tools.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/gappsscript/test_apps_script_tools.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
gappsscript/apps_script_tools.py (1)

40-76: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Raise UserInputError for file validation failures.

update_script_content wraps _merge_script_files, and handle_http_errors treats plain Exception/ValueError as unexpected tool errors with logger.exception, while it logs UserInputError warnings and re-raises it. These missing or ambiguous file names/types are expected user input failures, so switch these two raises to UserInputError after importing it from core.utils.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@gappsscript/apps_script_tools.py` around lines 40 - 76, Update
_merge_script_files to import and raise UserInputError from core.utils instead
of ValueError for both missing file names and missing or ambiguous types.
Preserve the existing validation messages and conditions so
update_script_content and handle_http_errors classify these user input failures
correctly.
🧹 Nitpick comments (1)
gappsscript/apps_script_tools.py (1)

20-28: 🩺 Stability & Availability | 🔵 Trivial

Per-process lock only — won't serialize across multiple workers/instances.

_SCRIPT_UPDATE_LOCKS is process-local, so if this server ever runs with multiple worker processes/replicas, concurrent merges for the same script_id from different processes can still race (lost update), even though single-process races are now correctly serialized. Worth documenting as a known limitation if multi-process/multi-instance deployment is supported.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@gappsscript/apps_script_tools.py` around lines 20 - 28, Document near
_SCRIPT_UPDATE_LOCKS and _get_script_update_lock that the lock only serializes
updates within a single process and does not coordinate multiple worker
processes or service instances; explicitly note that multi-instance deployments
can still race when merging the same script_id.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@gappsscript/apps_script_tools.py`:
- Around line 40-76: Update _merge_script_files to import and raise
UserInputError from core.utils instead of ValueError for both missing file names
and missing or ambiguous types. Preserve the existing validation messages and
conditions so update_script_content and handle_http_errors classify these user
input failures correctly.

---

Nitpick comments:
In `@gappsscript/apps_script_tools.py`:
- Around line 20-28: Document near _SCRIPT_UPDATE_LOCKS and
_get_script_update_lock that the lock only serializes updates within a single
process and does not coordinate multiple worker processes or service instances;
explicitly note that multi-instance deployments can still race when merging the
same script_id.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: eb9f13c2-6a4e-434c-a122-8aa5189abcda

📥 Commits

Reviewing files that changed from the base of the PR and between 037851e and a200635.

📒 Files selected for processing (2)
  • gappsscript/apps_script_tools.py
  • tests/gappsscript/test_apps_script_tools.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
gappsscript/apps_script_tools.py (3)

20-29: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Add concurrency guards for remote merges.

getContent/updateContent is still a server-side read-modify-replace, and this asyncio.Lock, even with process isolation noted, does not serialize callers in other workers or service instances. Concurrency across those boundaries can cause the later update to discard another writer’s changes. Add distributed/remote locking or explicit single-writer guarantees if update_script_content is expected to be used concurrently.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@gappsscript/apps_script_tools.py` around lines 20 - 29, Add a distributed
lock or equivalent single-writer guarantee around the remote read-modify-replace
flow in update_script_content, covering both getContent and updateContent so
callers across workers or service instances cannot overwrite one another’s
changes. Retain the existing _get_script_update_lock as an in-process guard, and
ensure the remote lock is acquired before reading and held through the update.

60-76: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Validate Apps Script file types before building merge keys.

_merge_script_files() currently accepts arbitrary non-null type values and can forward them to the API, including JSON for non-manifest files. Raise UserInputError for explicit types outside SERVER_JS, HTML, and manifest JSON, and validate that manifest JSON is named appsscript before inserting into merged.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@gappsscript/apps_script_tools.py` around lines 60 - 76, Update
_merge_script_files() to validate each explicit file type before constructing
the merge key: allow only SERVER_JS, HTML, and JSON, and require JSON files to
use the manifest name appsscript; raise UserInputError for unsupported types or
invalid JSON names, then proceed with key creation and merging only after
validation.

32-39: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Do not copy explicit None values into normalized files.

_normalize_script_file currently copies any present key, so "type": None survives normalization. After merge inference, line 76 overwrites the existing file type with None, producing an invalid Apps Script updateContent File payload since type should be SERVER_JS, HTML, or JSON. Filter out None values or reject explicit nulls consistently, and add a regression test.

Proposed fix
-    return {key: file[key] for key in ("name", "type", "source") if key in file}
+    return {
+        key: file[key]
+        for key in ("name", "type", "source")
+        if key in file and file[key] is not None
+    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@gappsscript/apps_script_tools.py` around lines 32 - 39, Update
_normalize_script_file to exclude fields whose values are None, ensuring
normalized files never propagate explicit nulls for name, type, or source;
preserve omitted fields for merge inference. Add a regression test covering a
file with "type": None and verify the merged update retains the existing valid
type.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@gappsscript/apps_script_tools.py`:
- Around line 20-29: Add a distributed lock or equivalent single-writer
guarantee around the remote read-modify-replace flow in update_script_content,
covering both getContent and updateContent so callers across workers or service
instances cannot overwrite one another’s changes. Retain the existing
_get_script_update_lock as an in-process guard, and ensure the remote lock is
acquired before reading and held through the update.
- Around line 60-76: Update _merge_script_files() to validate each explicit file
type before constructing the merge key: allow only SERVER_JS, HTML, and JSON,
and require JSON files to use the manifest name appsscript; raise UserInputError
for unsupported types or invalid JSON names, then proceed with key creation and
merging only after validation.
- Around line 32-39: Update _normalize_script_file to exclude fields whose
values are None, ensuring normalized files never propagate explicit nulls for
name, type, or source; preserve omitted fields for merge inference. Add a
regression test covering a file with "type": None and verify the merged update
retains the existing valid type.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c9fb9872-06d9-46a5-b36b-2e280cf82c7f

📥 Commits

Reviewing files that changed from the base of the PR and between a200635 and dd663ac.

📒 Files selected for processing (2)
  • gappsscript/apps_script_tools.py
  • tests/gappsscript/test_apps_script_tools.py

@taylorwilsdon
taylorwilsdon merged commit 898085b into taylorwilsdon:main Jul 30, 2026
6 of 7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

update_script_content silently DELETES files omitted from the files list — the tool description does not say so

2 participants