Skip to content

Commit 8a749c2

Browse files
authored
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.
1 parent f157208 commit 8a749c2

File tree

3 files changed

+99
-21
lines changed

3 files changed

+99
-21
lines changed
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
"@googleworkspace/cli": patch
3+
---
4+
5+
feat(helpers): add --dry-run support to events helper commands
6+
7+
Add dry-run mode to `gws events +renew` and `gws events +subscribe` commands.
8+
When --dry-run is specified, the commands will print what actions would be
9+
taken without making any API calls. This allows agents to simulate requests
10+
and learn without reaching the server.

src/helpers/events/renew.rs

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,40 @@ pub(super) async fn handle_renew(
3030
matches: &ArgMatches,
3131
) -> Result<(), GwsError> {
3232
let config = parse_renew_args(matches)?;
33+
let dry_run = matches.get_flag("dry-run");
34+
35+
if dry_run {
36+
eprintln!("🏃 DRY RUN — no changes will be made\n");
37+
38+
// Handle dry-run case and exit early
39+
let result = if let Some(name) = config.name {
40+
let name = crate::validate::validate_resource_name(&name)?;
41+
eprintln!("Reactivating subscription: {name}");
42+
json!({
43+
"dry_run": true,
44+
"action": "Would reactivate subscription",
45+
"name": name,
46+
"note": "Run without --dry-run to actually reactivate the subscription"
47+
})
48+
} else {
49+
json!({
50+
"dry_run": true,
51+
"action": "Would list and renew subscriptions expiring within",
52+
"within": config.within,
53+
"note": "Run without --dry-run to actually renew subscriptions"
54+
})
55+
};
56+
println!("{}", serde_json::to_string_pretty(&result).context("Failed to serialize dry-run output")?);
57+
return Ok(());
58+
}
59+
60+
// Real run logic
3361
let client = crate::client::build_client()?;
3462
let ws_token = auth::get_token(&[WORKSPACE_EVENTS_SCOPE])
3563
.await
3664
.map_err(|e| GwsError::Auth(format!("Failed to get token: {e}")))?;
3765

3866
if let Some(name) = config.name {
39-
// Reactivate a specific subscription
4067
let name = crate::validate::validate_resource_name(&name)?;
4168
eprintln!("Reactivating subscription: {name}");
4269
let resp = client
@@ -51,15 +78,9 @@ pub(super) async fn handle_renew(
5178
.context("Failed to reactivate subscription")?;
5279

5380
let body: Value = resp.json().await.context("Failed to parse response")?;
54-
55-
println!(
56-
"{}",
57-
serde_json::to_string_pretty(&body).unwrap_or_default()
58-
);
81+
println!("{}", serde_json::to_string_pretty(&body).context("Failed to serialize response body")?);
5982
} else {
6083
let within_secs = parse_duration(&config.within)?;
61-
62-
// List all subscriptions
6384
let resp = client
6485
.get("https://workspaceevents.googleapis.com/v1/subscriptions")
6586
.bearer_auth(&ws_token)
@@ -98,10 +119,7 @@ pub(super) async fn handle_renew(
98119
"status": "success",
99120
"renewed": renewed,
100121
});
101-
println!(
102-
"{}",
103-
serde_json::to_string_pretty(&result).unwrap_or_default()
104-
);
122+
println!("{}", serde_json::to_string_pretty(&result).context("Failed to serialize result")?);
105123
}
106124

107125
Ok(())

src/helpers/events/subscribe.rs

Lines changed: 59 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -107,26 +107,53 @@ pub(super) async fn handle_subscribe(
107107
matches: &ArgMatches,
108108
) -> Result<(), GwsError> {
109109
let config = parse_subscribe_args(matches)?;
110+
let dry_run = matches.get_flag("dry-run");
111+
112+
if dry_run {
113+
eprintln!("🏃 DRY RUN — no changes will be made\n");
114+
}
110115

111116
if let Some(ref dir) = config.output_dir {
112-
std::fs::create_dir_all(dir).context("Failed to create output dir")?;
117+
if !dry_run {
118+
std::fs::create_dir_all(dir).context("Failed to create output dir")?;
119+
}
113120
}
114121

115122
let client = crate::client::build_client()?;
116123
let pubsub_token_provider = auth::token_provider(&[PUBSUB_SCOPE]);
117124

118-
// Get Pub/Sub token
119-
let pubsub_token = auth::get_token(&[PUBSUB_SCOPE])
120-
.await
121-
.map_err(|e| GwsError::Auth(format!("Failed to get Pub/Sub token: {e}")))?;
122-
123125
let (pubsub_subscription, topic_name, ws_subscription_name, created_resources) =
124126
if let Some(ref sub_name) = config.subscription {
125127
// Use existing subscription — no setup needed
128+
// (don't fetch Pub/Sub token since we won't need it for existing subscriptions)
129+
if dry_run {
130+
eprintln!("Would listen to existing subscription: {}", sub_name.0);
131+
let result = json!({
132+
"dry_run": true,
133+
"action": "Would listen to existing subscription",
134+
"subscription": sub_name.0,
135+
"note": "Run without --dry-run to actually start listening"
136+
});
137+
println!("{}", serde_json::to_string_pretty(&result).context("Failed to serialize dry-run output")?);
138+
return Ok(());
139+
}
126140
(sub_name.0.clone(), None, None, false)
127141
} else {
142+
// Get Pub/Sub token only when creating new subscription
143+
let pubsub_token = if dry_run {
144+
None
145+
} else {
146+
Some(
147+
auth::get_token(&[PUBSUB_SCOPE])
148+
.await
149+
.map_err(|e| GwsError::Auth(format!("Failed to get Pub/Sub token: {e}")))?,
150+
)
151+
};
152+
128153
// Full setup: create Pub/Sub topic + subscription + Workspace Events subscription
129-
let target = config.target.clone().unwrap();
154+
// Validate target before use in both dry-run and actual execution paths
155+
let target = crate::validate::validate_resource_name(&config.target.clone().unwrap())?
156+
.to_string();
130157
let project =
131158
crate::validate::validate_resource_name(&config.project.clone().unwrap().0)?
132159
.to_string();
@@ -140,11 +167,34 @@ pub(super) async fn handle_subscribe(
140167
let topic = format!("projects/{project}/topics/gws-{slug}-{suffix}");
141168
let sub = format!("projects/{project}/subscriptions/gws-{slug}-{suffix}");
142169

170+
// Dry-run: print what would be created and exit
171+
if dry_run {
172+
eprintln!("Would create Pub/Sub topic: {topic}");
173+
eprintln!("Would create Pub/Sub subscription: {sub}");
174+
eprintln!("Would create Workspace Events subscription for target: {target}");
175+
eprintln!("Would listen for event types: {}", config.event_types.join(", "));
176+
177+
let result = json!({
178+
"dry_run": true,
179+
"action": "Would create Workspace Events subscription",
180+
"pubsub_topic": topic,
181+
"pubsub_subscription": sub,
182+
"target": target,
183+
"event_types": config.event_types,
184+
"note": "Run without --dry-run to actually create subscription"
185+
});
186+
println!("{}", serde_json::to_string_pretty(&result).context("Failed to serialize dry-run output")?);
187+
return Ok(());
188+
}
189+
143190
// 1. Create Pub/Sub topic
144191
eprintln!("Creating Pub/Sub topic: {topic}");
192+
let token = pubsub_token
193+
.as_ref()
194+
.ok_or_else(|| GwsError::Auth("Token unavailable in non-dry-run mode. This indicates a bug.".to_string()))?;
145195
let resp = client
146196
.put(format!("{PUBSUB_API_BASE}/{topic}"))
147-
.bearer_auth(&pubsub_token)
197+
.bearer_auth(token)
148198
.header("Content-Type", "application/json")
149199
.body("{}")
150200
.send()
@@ -169,7 +219,7 @@ pub(super) async fn handle_subscribe(
169219
});
170220
let resp = client
171221
.put(format!("{PUBSUB_API_BASE}/{sub}"))
172-
.bearer_auth(&pubsub_token)
222+
.bearer_auth(token)
173223
.header("Content-Type", "application/json")
174224
.json(&sub_body)
175225
.send()

0 commit comments

Comments
 (0)