Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .changeset/add-dry-run-to-event-helpers.md
Original file line number Diff line number Diff line change
@@ -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.
42 changes: 30 additions & 12 deletions src/helpers/events/renew.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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(())
Expand Down
68 changes: 59 additions & 9 deletions src/helpers/events/subscribe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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()
Expand All @@ -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()
Expand Down
Loading