feat: add --dry-run support to events helper commands (#457)

Add dry-run mode to gws events +renew and gws events +subscribe commands.
When --dry-run is specified, the commands print what actions would be
taken without making any API calls. This allows agents to simulate
requests and learn without reaching the server.
This commit is contained in:
JoeVenner
2026-03-23 21:38:17 +01:00
committed by GitHub
parent f157208449
commit 8a749c2c11
3 changed files with 99 additions and 21 deletions
@@ -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.
+30 -12
View File
@@ -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(())
+59 -9
View File
@@ -107,26 +107,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();
@@ -140,11 +167,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()
@@ -169,7 +219,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()