@@ -31,31 +31,48 @@ pub enum TimezoneConfig {
3131 Named ( Tz ) ,
3232}
3333
34- /// Webhook configuration - either a URL or Discord id/token pair
34+ /// Webhook configuration for failure notifications
3535#[ derive( Debug , Clone , PartialEq , Deserialize ) ]
36- #[ serde( untagged) ]
37- pub enum WebhookConfig {
38- Url { url : String } ,
39- Discord { id : String , token : String } ,
36+ pub struct WebhookConfig {
37+ /// Webhook type (currently only "discord" supported)
38+ #[ serde( rename = "type" , default = "default_webhook_type" ) ]
39+ pub webhook_type : String ,
40+ /// Webhook URL (supports $ENV_VAR expansion)
41+ pub url : String ,
42+ }
43+
44+ fn default_webhook_type ( ) -> String {
45+ "discord" . to_string ( )
4046}
4147
4248impl WebhookConfig {
43- /// Convert to URL string, expanding environment variables
44- pub fn to_url ( & self ) -> String {
45- match self {
46- WebhookConfig :: Url { url } => shellexpand:: full ( url)
47- . map ( |s| s. into_owned ( ) )
48- . unwrap_or_else ( |_| url. clone ( ) ) ,
49- WebhookConfig :: Discord { id, token } => {
50- let id = shellexpand:: full ( id)
51- . map ( |s| s. into_owned ( ) )
52- . unwrap_or_else ( |_| id. clone ( ) ) ;
53- let token = shellexpand:: full ( token)
54- . map ( |s| s. into_owned ( ) )
55- . unwrap_or_else ( |_| token. clone ( ) ) ;
56- format ! ( "https://discord.com/api/webhooks/{}/{}" , id, token)
57- }
49+ /// Convert to URL string, expanding environment variables.
50+ /// If env_vars is provided, uses those for $VAR expansion.
51+ /// Falls back to process environment for undefined variables.
52+ pub fn to_url ( & self , env_vars : Option < & std:: collections:: HashMap < String , String > > ) -> String {
53+ expand_with_env ( & self . url , env_vars)
54+ }
55+ }
56+
57+ /// Expand shell-like variables in a string.
58+ /// Uses provided env_vars first, then falls back to process environment.
59+ fn expand_with_env (
60+ s : & str ,
61+ env_vars : Option < & std:: collections:: HashMap < String , String > > ,
62+ ) -> String {
63+ match env_vars {
64+ Some ( vars) => {
65+ let home_dir = || dirs:: home_dir ( ) . map ( |p| p. to_string_lossy ( ) . into_owned ( ) ) ;
66+ shellexpand:: full_with_context_no_errors ( s, home_dir, |var| {
67+ vars. get ( var)
68+ . map ( |v| std:: borrow:: Cow :: Borrowed ( v. as_str ( ) ) )
69+ . or_else ( || std:: env:: var ( var) . ok ( ) . map ( std:: borrow:: Cow :: Owned ) )
70+ } )
71+ . into_owned ( )
5872 }
73+ None => shellexpand:: full ( s)
74+ . map ( |s| s. into_owned ( ) )
75+ . unwrap_or_else ( |_| s. to_string ( ) ) ,
5976 }
6077}
6178
@@ -900,7 +917,7 @@ jobs:
900917 let ( _, jobs) = parse_config ( yaml) . unwrap ( ) ;
901918 // Job inherits runner webhook
902919 assert_eq ! ( jobs[ 0 ] . webhook. len( ) , 1 ) ;
903- assert_eq ! ( jobs[ 0 ] . webhook[ 0 ] . to_url( ) , "https://hooks.slack.com/test" ) ;
920+ assert_eq ! ( jobs[ 0 ] . webhook[ 0 ] . to_url( None ) , "https://hooks.slack.com/test" ) ;
904921 }
905922
906923 #[ test]
@@ -944,13 +961,13 @@ jobs:
944961 cron: "* * * * *"
945962 run: echo test
946963 webhook:
947- - id: "123456"
948- token: "abcdef"
964+ - url: https://discord.com/api/webhooks/123456/abcdef
949965"# ;
950966 let ( _, jobs) = parse_config ( yaml) . unwrap ( ) ;
951967 assert_eq ! ( jobs[ 0 ] . webhook. len( ) , 1 ) ;
968+ assert_eq ! ( jobs[ 0 ] . webhook[ 0 ] . webhook_type, "discord" ) ;
952969 assert_eq ! (
953- jobs[ 0 ] . webhook[ 0 ] . to_url( ) ,
970+ jobs[ 0 ] . webhook[ 0 ] . to_url( None ) ,
954971 "https://discord.com/api/webhooks/123456/abcdef"
955972 ) ;
956973 }
@@ -966,11 +983,46 @@ jobs:
966983 webhook:
967984 - url: https://hooks.slack.com/first
968985 - url: https://hooks.slack.com/second
969- - id: discord_id
970- token: discord_token
986+ - type: discord
987+ url: https://discord.com/api/webhooks/id/token
971988"# ;
972989 let ( _, jobs) = parse_config ( yaml) . unwrap ( ) ;
973990 assert_eq ! ( jobs[ 0 ] . webhook. len( ) , 3 ) ;
974991 }
975992
993+ #[ test]
994+ fn parse_webhook_with_explicit_type ( ) {
995+ let yaml = r#"
996+ jobs:
997+ test:
998+ schedule:
999+ cron: "* * * * *"
1000+ run: echo test
1001+ webhook:
1002+ - type: discord
1003+ url: https://discord.com/api/webhooks/test
1004+ "# ;
1005+ let ( _, jobs) = parse_config ( yaml) . unwrap ( ) ;
1006+ assert_eq ! ( jobs[ 0 ] . webhook. len( ) , 1 ) ;
1007+ assert_eq ! ( jobs[ 0 ] . webhook[ 0 ] . webhook_type, "discord" ) ;
1008+ }
1009+
1010+ #[ test]
1011+ fn webhook_env_var_expansion ( ) {
1012+ let mut env_vars = HashMap :: new ( ) ;
1013+ env_vars. insert (
1014+ "DISCORD_WEBHOOK" . to_string ( ) ,
1015+ "https://discord.com/api/webhooks/from_env" . to_string ( ) ,
1016+ ) ;
1017+
1018+ let webhook = WebhookConfig {
1019+ webhook_type : "discord" . to_string ( ) ,
1020+ url : "$DISCORD_WEBHOOK" . to_string ( ) ,
1021+ } ;
1022+
1023+ assert_eq ! (
1024+ webhook. to_url( Some ( & env_vars) ) ,
1025+ "https://discord.com/api/webhooks/from_env"
1026+ ) ;
1027+ }
9761028}
0 commit comments