From da880fd23c5b2259ded8c025745e8b90136a6870 Mon Sep 17 00:00:00 2001 From: seang1121 Date: Sat, 14 Mar 2026 01:42:50 -0400 Subject: [PATCH 1/2] fix(drive): warn when anchor field targets Workspace editor files (#169) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When `gws drive comments create` includes an `anchor` field, the helper now checks the target file's mimeType via `files.get`. If the file is a Google Workspace editor file (Docs, Sheets, Slides, Drawings), a warning is printed to stderr explaining that the anchor will be silently ignored by the editor UI and the comment may show as "Original content deleted". If the mimeType cannot be determined (e.g., unauthenticated), a general warning is printed instead. The command still executes — this is a non-blocking warning only. Fixes #169 --- .changeset/warn-anchored-comments.md | 5 + src/helpers/drive.rs | 188 ++++++++++++++++++++++++++- 2 files changed, 192 insertions(+), 1 deletion(-) create mode 100644 .changeset/warn-anchored-comments.md diff --git a/.changeset/warn-anchored-comments.md b/.changeset/warn-anchored-comments.md new file mode 100644 index 00000000..563fd9dd --- /dev/null +++ b/.changeset/warn-anchored-comments.md @@ -0,0 +1,5 @@ +--- +"@googleworkspace/cli": patch +--- + +Warn when `drive comments create` uses an `anchor` field on Google Workspace editor files (Docs, Sheets, Slides, Drawings), where anchors are silently ignored by the editor UI diff --git a/src/helpers/drive.rs b/src/helpers/drive.rs index 393e0fde..b9a882d6 100644 --- a/src/helpers/drive.rs +++ b/src/helpers/drive.rs @@ -14,7 +14,7 @@ use super::Helper; use crate::auth; -use crate::error::GwsError; +use crate::error::{self, GwsError}; use crate::executor; use clap::{Arg, ArgMatches, Command}; use serde_json::{json, Value}; @@ -22,6 +22,16 @@ use std::future::Future; use std::path::Path; use std::pin::Pin; +/// MIME types for Google Workspace editor files where the Drive API `anchor` +/// field on comments is saved but silently ignored by the editor UI. +/// See: https://developers.google.com/workspace/drive/api/v3/manage-comments +const WORKSPACE_EDITOR_MIMES: &[&str] = &[ + "application/vnd.google-apps.document", + "application/vnd.google-apps.spreadsheet", + "application/vnd.google-apps.presentation", + "application/vnd.google-apps.drawing", +]; + pub struct DriveHelper; impl Helper for DriveHelper { @@ -73,6 +83,9 @@ TIPS: _sanitize_config: &'a crate::helpers::modelarmor::SanitizeConfig, ) -> Pin> + Send + 'a>> { Box::pin(async move { + // Check for anchored comments on Workspace editor files (#169) + warn_anchored_comment_if_needed(doc, matches).await; + if let Some(matches) = matches.subcommand_matches("+upload") { let file_path = matches.get_one::("file").unwrap(); let parent_id = matches.get_one::("parent"); @@ -127,6 +140,139 @@ TIPS: } } +/// Warn when `comments create` includes an `anchor` field targeting a Workspace +/// editor file (Docs, Sheets, Slides, Drawings). The Drive API accepts the anchor +/// but the editor UI silently ignores it, showing "Original content deleted". +/// +/// This does not block execution — the dynamic dispatcher still runs the request. +async fn warn_anchored_comment_if_needed( + doc: &crate::discovery::RestDescription, + matches: &ArgMatches, +) { + // Walk the dynamic subcommand tree: comments → create + let create_matches = matches + .subcommand_matches("comments") + .and_then(|m| m.subcommand_matches("create")); + + let create_matches = match create_matches { + Some(m) => m, + None => return, + }; + + // Check if the --json body contains an "anchor" field + let body_str = match create_matches.try_get_one::("json").ok().flatten() { + Some(s) => s.as_str(), + None => return, + }; + + let body: Value = match serde_json::from_str(body_str) { + Ok(v) => v, + Err(_) => return, // Invalid JSON will be caught later by the executor + }; + + if body.get("anchor").is_none() { + return; + } + + // Extract fileId from --params + let file_id = create_matches + .get_one::("params") + .and_then(|p| serde_json::from_str::(p).ok()) + .and_then(|v| v.get("fileId").and_then(|id| id.as_str()).map(String::from)); + + let file_id = match file_id { + Some(id) => id, + None => return, // Missing fileId will be caught later by the executor + }; + + // Try to fetch the file's mimeType to give a precise warning. + // If auth or the request fails, fall back to a general warning. + match fetch_file_mime_type(doc, &file_id).await { + Ok(mime) if is_workspace_editor_mime(&mime) => { + eprintln!( + "\n{}", + error::yellow(&format!( + "⚠️ Warning: anchor field ignored for {} files.\n \ + Google Workspace editors treat anchored comments as un-anchored.\n \ + The comment will be created but may show as \"Original content deleted\".\n \ + See: https://developers.google.com/workspace/drive/api/v3/manage-comments\n", + mime_display_name(&mime), + )), + ); + } + Ok(_) => {} // Non-editor file — anchor should work fine + Err(_) => { + // Could not determine file type; print a general warning + eprintln!( + "\n{}", + error::yellow( + "⚠️ Warning: anchor field may be ignored for Google Workspace editor files \ + (Docs, Sheets, Slides).\n \ + If the target is a Workspace file, the comment may show as \ + \"Original content deleted\".\n \ + See: https://developers.google.com/workspace/drive/api/v3/manage-comments\n", + ), + ); + } + } +} + +/// Fetch the mimeType of a Drive file by ID. +async fn fetch_file_mime_type( + doc: &crate::discovery::RestDescription, + file_id: &str, +) -> Result { + let files_res = doc + .resources + .get("files") + .ok_or_else(|| GwsError::Discovery("Resource 'files' not found".to_string()))?; + let get_method = files_res + .methods + .get("get") + .ok_or_else(|| GwsError::Discovery("Method 'files.get' not found".to_string()))?; + + let scopes: Vec<&str> = get_method.scopes.iter().map(|s| s.as_str()).collect(); + let token = auth::get_token(&scopes).await?; + + let params = format!(r#"{{"fileId":"{}","fields":"mimeType"}}"#, file_id); + let output = executor::execute_method( + doc, + get_method, + Some(¶ms), + None, + Some(&token), + executor::AuthMethod::OAuth, + None, + None, + false, + &executor::PaginationConfig::default(), + None, + &crate::helpers::modelarmor::SanitizeMode::Warn, + &crate::formatter::OutputFormat::default(), + true, // capture_output — don't print to stdout + ) + .await?; + + // extract mimeType from captured output + output + .and_then(|v| v.get("mimeType").and_then(|m| m.as_str()).map(String::from)) + .ok_or_else(|| GwsError::Other(anyhow::anyhow!("mimeType not found in response"))) +} + +fn is_workspace_editor_mime(mime: &str) -> bool { + WORKSPACE_EDITOR_MIMES.contains(&mime) +} + +fn mime_display_name(mime: &str) -> &str { + match mime { + "application/vnd.google-apps.document" => "Google Docs", + "application/vnd.google-apps.spreadsheet" => "Google Sheets", + "application/vnd.google-apps.presentation" => "Google Slides", + "application/vnd.google-apps.drawing" => "Google Drawings", + _ => "Google Workspace", + } +} + fn determine_filename(file_path: &str, name_arg: Option<&str>) -> Result { if let Some(n) = name_arg { Ok(n.to_string()) @@ -190,4 +336,44 @@ mod tests { assert_eq!(meta["name"], "file.txt"); assert_eq!(meta["parents"][0], "folder123"); } + + #[test] + fn test_is_workspace_editor_mime() { + assert!(is_workspace_editor_mime( + "application/vnd.google-apps.document" + )); + assert!(is_workspace_editor_mime( + "application/vnd.google-apps.spreadsheet" + )); + assert!(is_workspace_editor_mime( + "application/vnd.google-apps.presentation" + )); + assert!(is_workspace_editor_mime( + "application/vnd.google-apps.drawing" + )); + assert!(!is_workspace_editor_mime("application/pdf")); + assert!(!is_workspace_editor_mime("image/png")); + assert!(!is_workspace_editor_mime("text/plain")); + } + + #[test] + fn test_mime_display_name() { + assert_eq!( + mime_display_name("application/vnd.google-apps.document"), + "Google Docs" + ); + assert_eq!( + mime_display_name("application/vnd.google-apps.spreadsheet"), + "Google Sheets" + ); + assert_eq!( + mime_display_name("application/vnd.google-apps.presentation"), + "Google Slides" + ); + assert_eq!( + mime_display_name("application/vnd.google-apps.drawing"), + "Google Drawings" + ); + assert_eq!(mime_display_name("application/pdf"), "Google Workspace"); + } } From e04e13871e630d343a6de6c884deede709b23e33 Mon Sep 17 00:00:00 2001 From: seang1121 Date: Sat, 14 Mar 2026 01:57:03 -0400 Subject: [PATCH 2/2] fix(ci): replace missing GOOGLEWORKSPACE_BOT_TOKEN with GITHUB_TOKEN The upstream bot token secret doesn't exist in this fork, causing all workflows to fail at checkout with "Input required and not supplied: token". --- .github/workflows/automation.yml | 6 +++--- .github/workflows/cla.yml | 2 +- .github/workflows/generate-skills.yml | 10 +++++----- .github/workflows/release-changesets.yml | 10 +++++----- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/.github/workflows/automation.yml b/.github/workflows/automation.yml index 5eb3de95..14248af9 100644 --- a/.github/workflows/automation.yml +++ b/.github/workflows/automation.yml @@ -35,7 +35,7 @@ jobs: steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: - token: ${{ secrets.GOOGLEWORKSPACE_BOT_TOKEN }} + token: ${{ secrets.GITHUB_TOKEN }} - name: Install Rust uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable @@ -59,7 +59,7 @@ jobs: steps: - uses: actions/labeler@8558fd74291d67161a8a78ce36a881fa63b766a9 # v5 with: - repo-token: ${{ secrets.GOOGLEWORKSPACE_BOT_TOKEN }} + repo-token: ${{ secrets.GITHUB_TOKEN }} sync-labels: true gemini-review: @@ -94,7 +94,7 @@ jobs: - name: Trigger Gemini Code Assist review uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 with: - github-token: ${{ secrets.GOOGLEWORKSPACE_BOT_TOKEN }} + github-token: ${{ secrets.GITHUB_TOKEN }} script: | await github.rest.issues.createComment({ owner: context.repo.owner, diff --git a/.github/workflows/cla.yml b/.github/workflows/cla.yml index c4a4809c..21e1c57d 100644 --- a/.github/workflows/cla.yml +++ b/.github/workflows/cla.yml @@ -30,7 +30,7 @@ jobs: - name: Update CLA label uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 with: - github-token: ${{ secrets.GOOGLEWORKSPACE_BOT_TOKEN }} + github-token: ${{ secrets.GITHUB_TOKEN }} script: | const cr = context.payload.check_run; const passed = cr.conclusion === 'success'; diff --git a/.github/workflows/generate-skills.yml b/.github/workflows/generate-skills.yml index a30f1bc3..120a26f9 100644 --- a/.github/workflows/generate-skills.yml +++ b/.github/workflows/generate-skills.yml @@ -42,7 +42,7 @@ jobs: with: # For cron/dispatch: check out main. For push: check out the branch. ref: ${{ github.head_ref || github.ref_name }} - token: ${{ secrets.GOOGLEWORKSPACE_BOT_TOKEN }} + token: ${{ secrets.GITHUB_TOKEN }} - name: Install Rust uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable @@ -88,7 +88,7 @@ jobs: (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') uses: peter-evans/create-pull-request@22a9089034f40e5a961c8808d113e2c98fb63676 # v7 with: - token: ${{ secrets.GOOGLEWORKSPACE_BOT_TOKEN }} + token: ${{ secrets.GITHUB_TOKEN }} branch: chore/sync-skills title: "chore: sync skills with Discovery API" body: | @@ -109,10 +109,10 @@ jobs: steps.diff.outputs.changed == 'true' && github.event_name == 'push' env: - GITHUB_TOKEN: ${{ secrets.GOOGLEWORKSPACE_BOT_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - git config user.name "googleworkspace-bot" - git config user.email "googleworkspace-bot@users.noreply.github.com" + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" git add skills/ docs/skills.md git commit -m "chore: regenerate skills [skip ci]" diff --git a/.github/workflows/release-changesets.yml b/.github/workflows/release-changesets.yml index 25caa4f4..c766c282 100644 --- a/.github/workflows/release-changesets.yml +++ b/.github/workflows/release-changesets.yml @@ -34,7 +34,7 @@ jobs: steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: - token: ${{ secrets.GOOGLEWORKSPACE_BOT_TOKEN }} + token: ${{ secrets.GITHUB_TOKEN }} - name: Install Rust uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable @@ -42,7 +42,7 @@ jobs: - name: Install Nix uses: cachix/install-nix-action@08dcb3a5e62fa31e2da3d490afc4176ef55ecd72 # v30 with: - github_access_token: ${{ secrets.GOOGLEWORKSPACE_BOT_TOKEN }} + github_access_token: ${{ secrets.GITHUB_TOKEN }} - uses: pnpm/action-setup@c5ba7f7862a0f64c1b1a05fbac13e0b8e86ba08c # v4 @@ -56,8 +56,8 @@ jobs: run: pnpm install - run: | - git config --global user.name "googleworkspace-bot" - git config --global user.email "googleworkspace-bot@google.com" + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" - name: Create Release Pull Request or Tag id: changesets @@ -69,4 +69,4 @@ jobs: title: 'chore: release versions' setupGitUser: false env: - GITHUB_TOKEN: ${{ secrets.GOOGLEWORKSPACE_BOT_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}