diff --git a/.changeset/add-dry-run-to-event-helpers.md b/.changeset/add-dry-run-to-event-helpers.md new file mode 100644 index 00000000..ce995185 --- /dev/null +++ b/.changeset/add-dry-run-to-event-helpers.md @@ -0,0 +1,10 @@ +--- +"@googleworkspace/cli": patch +--- + +feat(helpers): add --dry-run support to events helper commands + +Add dry-run mode to `gws events +renew` and `gws events +subscribe` commands. +When --dry-run is specified, the commands will print what actions would be +taken without making any API calls. This allows agents to simulate requests +and learn without reaching the server. diff --git a/src/helpers/events/renew.rs b/src/helpers/events/renew.rs index 3dd1d627..0ae5affe 100644 --- a/src/helpers/events/renew.rs +++ b/src/helpers/events/renew.rs @@ -30,13 +30,40 @@ pub(super) async fn handle_renew( matches: &ArgMatches, ) -> Result<(), GwsError> { let config = parse_renew_args(matches)?; + let dry_run = matches.get_flag("dry-run"); + + if dry_run { + eprintln!("🏃 DRY RUN — no changes will be made\n"); + + // Handle dry-run case and exit early + let result = if let Some(name) = config.name { + let name = crate::validate::validate_resource_name(&name)?; + eprintln!("Reactivating subscription: {name}"); + json!({ + "dry_run": true, + "action": "Would reactivate subscription", + "name": name, + "note": "Run without --dry-run to actually reactivate the subscription" + }) + } else { + json!({ + "dry_run": true, + "action": "Would list and renew subscriptions expiring within", + "within": config.within, + "note": "Run without --dry-run to actually renew subscriptions" + }) + }; + println!("{}", serde_json::to_string_pretty(&result).context("Failed to serialize dry-run output")?); + return Ok(()); + } + + // Real run logic let client = crate::client::build_client()?; let ws_token = auth::get_token(&[WORKSPACE_EVENTS_SCOPE]) .await .map_err(|e| GwsError::Auth(format!("Failed to get token: {e}")))?; if let Some(name) = config.name { - // Reactivate a specific subscription let name = crate::validate::validate_resource_name(&name)?; eprintln!("Reactivating subscription: {name}"); let resp = client @@ -51,15 +78,9 @@ pub(super) async fn handle_renew( .context("Failed to reactivate subscription")?; let body: Value = resp.json().await.context("Failed to parse response")?; - - println!( - "{}", - serde_json::to_string_pretty(&body).unwrap_or_default() - ); + println!("{}", serde_json::to_string_pretty(&body).context("Failed to serialize response body")?); } else { let within_secs = parse_duration(&config.within)?; - - // List all subscriptions let resp = client .get("https://workspaceevents.googleapis.com/v1/subscriptions") .bearer_auth(&ws_token) @@ -98,10 +119,7 @@ pub(super) async fn handle_renew( "status": "success", "renewed": renewed, }); - println!( - "{}", - serde_json::to_string_pretty(&result).unwrap_or_default() - ); + println!("{}", serde_json::to_string_pretty(&result).context("Failed to serialize result")?); } Ok(()) diff --git a/src/helpers/events/subscribe.rs b/src/helpers/events/subscribe.rs index edbfb4dc..a398cfbe 100644 --- a/src/helpers/events/subscribe.rs +++ b/src/helpers/events/subscribe.rs @@ -106,26 +106,53 @@ pub(super) async fn handle_subscribe( matches: &ArgMatches, ) -> Result<(), GwsError> { let config = parse_subscribe_args(matches)?; + let dry_run = matches.get_flag("dry-run"); + + if dry_run { + eprintln!("🏃 DRY RUN — no changes will be made\n"); + } if let Some(ref dir) = config.output_dir { - std::fs::create_dir_all(dir).context("Failed to create output dir")?; + if !dry_run { + std::fs::create_dir_all(dir).context("Failed to create output dir")?; + } } let client = crate::client::build_client()?; let pubsub_token_provider = auth::token_provider(&[PUBSUB_SCOPE]); - // Get Pub/Sub token - let pubsub_token = auth::get_token(&[PUBSUB_SCOPE]) - .await - .map_err(|e| GwsError::Auth(format!("Failed to get Pub/Sub token: {e}")))?; - let (pubsub_subscription, topic_name, ws_subscription_name, created_resources) = if let Some(ref sub_name) = config.subscription { // Use existing subscription — no setup needed + // (don't fetch Pub/Sub token since we won't need it for existing subscriptions) + if dry_run { + eprintln!("Would listen to existing subscription: {}", sub_name.0); + let result = json!({ + "dry_run": true, + "action": "Would listen to existing subscription", + "subscription": sub_name.0, + "note": "Run without --dry-run to actually start listening" + }); + println!("{}", serde_json::to_string_pretty(&result).context("Failed to serialize dry-run output")?); + return Ok(()); + } (sub_name.0.clone(), None, None, false) } else { + // Get Pub/Sub token only when creating new subscription + let pubsub_token = if dry_run { + None + } else { + Some( + auth::get_token(&[PUBSUB_SCOPE]) + .await + .map_err(|e| GwsError::Auth(format!("Failed to get Pub/Sub token: {e}")))?, + ) + }; + // Full setup: create Pub/Sub topic + subscription + Workspace Events subscription - let target = config.target.clone().unwrap(); + // Validate target before use in both dry-run and actual execution paths + let target = crate::validate::validate_resource_name(&config.target.clone().unwrap())? + .to_string(); let project = crate::validate::validate_resource_name(&config.project.clone().unwrap().0)? .to_string(); @@ -139,11 +166,34 @@ pub(super) async fn handle_subscribe( let topic = format!("projects/{project}/topics/gws-{slug}-{suffix}"); let sub = format!("projects/{project}/subscriptions/gws-{slug}-{suffix}"); + // Dry-run: print what would be created and exit + if dry_run { + eprintln!("Would create Pub/Sub topic: {topic}"); + eprintln!("Would create Pub/Sub subscription: {sub}"); + eprintln!("Would create Workspace Events subscription for target: {target}"); + eprintln!("Would listen for event types: {}", config.event_types.join(", ")); + + let result = json!({ + "dry_run": true, + "action": "Would create Workspace Events subscription", + "pubsub_topic": topic, + "pubsub_subscription": sub, + "target": target, + "event_types": config.event_types, + "note": "Run without --dry-run to actually create subscription" + }); + println!("{}", serde_json::to_string_pretty(&result).context("Failed to serialize dry-run output")?); + return Ok(()); + } + // 1. Create Pub/Sub topic eprintln!("Creating Pub/Sub topic: {topic}"); + let token = pubsub_token + .as_ref() + .ok_or_else(|| GwsError::Auth("Token unavailable in non-dry-run mode. This indicates a bug.".to_string()))?; let resp = client .put(format!("{PUBSUB_API_BASE}/{topic}")) - .bearer_auth(&pubsub_token) + .bearer_auth(token) .header("Content-Type", "application/json") .body("{}") .send() @@ -168,7 +218,7 @@ pub(super) async fn handle_subscribe( }); let resp = client .put(format!("{PUBSUB_API_BASE}/{sub}")) - .bearer_auth(&pubsub_token) + .bearer_auth(token) .header("Content-Type", "application/json") .json(&sub_body) .send()