@@ -222,17 +222,17 @@ func formatEnvKey(key string) string {
222222 return hypenOrSpaceRE .ReplaceAllString (newKey , "_" )
223223}
224224
225- func walkConfigValues ( prefix string , v any , into * [] string ) error {
225+ func flattenConfigToEnvMap ( into map [ string ] string , v any , envPrefix string ) error {
226226 switch vv := v .(type ) {
227227 // handles all of our primitive types, golang provides a good string representation
228228 case string , bool , json.Number :
229- * into = append ( * into , fmt .Sprintf ("%s=% v" , prefix , vv ) )
229+ into [ envPrefix ] = fmt .Sprintf ("%v" , vv )
230230 return nil
231231
232232 // handle lists of things, which get a KEY_N prefix depending on the index
233233 case []any :
234- for i := range vv {
235- if err := walkConfigValues ( fmt .Sprintf ("%s_%d" , prefix , i ), vv [ i ], into ); err != nil {
234+ for i , item := range vv {
235+ if err := flattenConfigToEnvMap ( into , item , fmt .Sprintf ("%s_%d" , envPrefix , i )); err != nil {
236236 return err
237237 }
238238 }
@@ -241,72 +241,62 @@ func walkConfigValues(prefix string, v any, into *[]string) error {
241241 // handle maps of things, which get a KEY_SUBKEY prefix depending on the map keys
242242 case map [string ]any :
243243 for k , vvv := range vv {
244- if err := walkConfigValues ( fmt .Sprintf ("%s_%s" , prefix , formatEnvKey (k )), vvv , into ); err != nil {
244+ if err := flattenConfigToEnvMap ( into , vvv , fmt .Sprintf ("%s_%s" , envPrefix , formatEnvKey (k ))); err != nil {
245245 return err
246246 }
247247 }
248248 return nil
249- }
250249
251- return fmt .Errorf ("Unknown type %T %v" , v , v )
250+ default :
251+ return fmt .Errorf ("Unknown type %T %v" , v , v )
252+ }
252253}
253254
254- // The input should be a slice of Env Variables of the form `k=v` where `k` is the variable name
255- // and `v` is its value. If it can be determined that any of the env variables names is part of a
256- // deprecation, either as the deprecated variable name or its replacement, append both to the
257- // returned slice, and also append a deprecation error to the error value. If there are no
258- // deprecations, the error value is guaranteed to be nil.
259- func fanOutDeprecatedEnvVarNames (envSlice []string ) ([]string , error ) {
260- envSliceAfter := envSlice
261-
262- var dnerrs * DeprecatedNameErrors
263- for _ , kv := range envSlice {
264- k , v , ok := strings .Cut (kv , "=" )
265- if ! ok { // this is impossible if the precondition is met
266- continue
267- }
268-
255+ // addDeprecatedEnvVarAliases provides backward compatibility for environment variable names.
256+ //
257+ // Before v3.48.0 (https://github.com/buildkite/agent/pull/2116), consecutive underscores in
258+ // derived env var names were collapsed (e.g., "some--key__name" → SOME_KEY_NAME).
259+ // Since v3.48.0, consecutive underscores are preserved (e.g., SOME__KEY__NAME).
260+ //
261+ // For compatibility, this function adds the collapsed form for any key containing consecutive
262+ // underscores and returns deprecation errors listing the affected variables.
263+ func addDeprecatedEnvVarAliases (envMap map [string ]string ) error {
264+ var errs * DeprecatedNameErrors
265+ for k , v := range envMap {
269266 // the form with consecutive underscores is replacing the form without, but the replacement
270- // is what is expected to be in input slice
271- noConsecutiveUnderScoreKey := consecutiveUnderscoreRE .ReplaceAllString (k , "_" )
272- if k != noConsecutiveUnderScoreKey {
273- envSliceAfter = append ( envSliceAfter , fmt . Sprintf ( "%s=%s" , noConsecutiveUnderScoreKey , v ))
274- dnerrs = dnerrs .Append (DeprecatedNameError {old : noConsecutiveUnderScoreKey , new : k })
267+ // is what is expected to be in input map
268+ withoutConsecutiveUnderscores := consecutiveUnderscoreRE .ReplaceAllString (k , "_" )
269+ if k != withoutConsecutiveUnderscores {
270+ envMap [ withoutConsecutiveUnderscores ] = v
271+ errs = errs .Append (DeprecatedNameError {old : withoutConsecutiveUnderscores , new : k })
275272 }
276273 }
277274
278- // guarantee that the error value is nil if there are no deprecations
279- if ! dnerrs .IsEmpty () {
280- return envSliceAfter , dnerrs
275+ if ! errs .IsEmpty () {
276+ return errs
281277 }
282- return envSliceAfter , nil
278+
279+ return nil
283280}
284281
285- // ConfigurationToEnvironment converts the plugin configuration values to
286- // environment variables.
282+ // ConfigurationToEnvironment converts the plugin configuration values to environment variables.
287283func (p * Plugin ) ConfigurationToEnvironment () (* env.Environment , error ) {
288- envSlice := []string {}
289- envPrefix := fmt .Sprintf ("BUILDKITE_PLUGIN_%s" , formatEnvKey (p .Name ()))
290-
291- // Append current plugin name
292- envSlice = append (envSlice , fmt .Sprintf ("BUILDKITE_PLUGIN_NAME=%s" , formatEnvKey (p .Name ())))
293-
294- // Append current plugin configuration as JSON
295284 configJSON , err := json .Marshal (p .Configuration )
296285 if err != nil {
297286 return env .New (), err
298287 }
299- envSlice = append (envSlice , fmt .Sprintf ("BUILDKITE_PLUGIN_CONFIGURATION=%s" , configJSON ))
300288
301- for k , v := range p .Configuration {
302- configPrefix := fmt .Sprintf ("%s_%s" , envPrefix , formatEnvKey (k ))
303- if err := walkConfigValues (configPrefix , v , & envSlice ); err != nil {
304- return env .New (), err
305- }
289+ envMap := make (map [string ]string )
290+ envMap ["BUILDKITE_PLUGIN_NAME" ] = formatEnvKey (p .Name ())
291+ envMap ["BUILDKITE_PLUGIN_CONFIGURATION" ] = string (configJSON )
292+
293+ envPrefix := fmt .Sprintf ("BUILDKITE_PLUGIN_%s" , formatEnvKey (p .Name ()))
294+ if err := flattenConfigToEnvMap (envMap , p .Configuration , envPrefix ); err != nil {
295+ return env .New (), err
306296 }
307297
308- envSlice , err = fanOutDeprecatedEnvVarNames ( envSlice )
309- return env .FromSlice ( envSlice ), err
298+ err = addDeprecatedEnvVarAliases ( envMap )
299+ return env .FromMap ( envMap ), err
310300}
311301
312302// Label returns a pretty name for the plugin.
0 commit comments