Skip to content

Commit 30e76ca

Browse files
wbrezaCopilot
andcommitted
fix: address code review findings for script provisioning provider
- Fix ContinueOnError bug: platform override no longer unconditionally resets the field when the override doesn't set it - Fix getAzdEnv: propagate context cancellation, warn on other errors instead of silently swallowing all failures - Add 10MB size limit on outputs.json to prevent OOM - Remove dead ScriptResult fields (Stdout/Stderr never populated) - Remove unused exported functions (OutputsToEnvMap, OutputsToProvisioning) - Extract toProtoOutputs helper to reduce duplication - Improve shBinary() portability: use LookPath with /bin/sh fallback - Add platform override tests (ContinueOnError preservation, env merge) - Add executor unit tests (buildShellCommand, mapToEnvSlice) - Fix hardcoded /tmp path in test to use t.TempDir() - Update README: clarify secrets are plain values in alpha Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 9b379f9 commit 30e76ca

File tree

8 files changed

+126
-59
lines changed

8 files changed

+126
-59
lines changed

cli/azd/extensions/microsoft.azd.scripts/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ infra:
4646
| `kind` | string | No | Script type: `sh` or `pwsh` (auto-detected from extension) |
4747
| `name` | string | No | Display name for progress reporting |
4848
| `env` | map | No | Environment variables with `${EXPRESSION}` substitution |
49-
| `secrets` | map | No | Secret references (`akvs://vault/secret`) |
49+
| `secrets` | map | No | Secret key-value pairs (plain values in alpha; Key Vault resolution planned) |
5050
| `continueOnError` | bool | No | Continue execution on script failure |
5151
| `windows` | object | No | Windows-specific overrides |
5252
| `posix` | object | No | Linux/macOS-specific overrides |

cli/azd/extensions/microsoft.azd.scripts/internal/provisioning/config.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,9 @@ func applyPlatformOverride(sc *ScriptConfig) {
178178
}
179179
maps.Copy(sc.Secrets, override.Secrets)
180180
}
181-
sc.ContinueOnError = override.ContinueOnError
181+
if override.ContinueOnError {
182+
sc.ContinueOnError = override.ContinueOnError
183+
}
182184
}
183185

184186
// normalizeKind resolves the script kind from the Kind, Shell, or file extension.

cli/azd/extensions/microsoft.azd.scripts/internal/provisioning/config_test.go

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ func TestParseProviderConfig_EmptyConfig(t *testing.T) {
5151
cfg, err := ParseProviderConfig(nil)
5252
require.NoError(t, err)
5353

54-
err = cfg.Validate("/tmp")
54+
err = cfg.Validate(t.TempDir())
5555
require.ErrorContains(t, err, "at least one")
5656
}
5757

@@ -177,6 +177,46 @@ func TestValidateScriptConfig_ContinueOnError(t *testing.T) {
177177
require.True(t, cfg.Provision[0].ContinueOnError)
178178
}
179179

180+
func TestApplyPlatformOverride_DoesNotResetContinueOnError(t *testing.T) {
181+
sc := &ScriptConfig{
182+
Kind: "sh",
183+
Run: "fallback.sh",
184+
ContinueOnError: true,
185+
Posix: &ScriptConfig{
186+
Run: "linux-specific.sh",
187+
},
188+
Windows: &ScriptConfig{
189+
Run: "windows-specific.sh",
190+
},
191+
}
192+
193+
applyPlatformOverride(sc)
194+
195+
// ContinueOnError should NOT be reset to false by a platform override
196+
// that doesn't explicitly set it.
197+
require.True(t, sc.ContinueOnError, "ContinueOnError should not be reset by platform override")
198+
}
199+
200+
func TestApplyPlatformOverride_MergesEnvMaps(t *testing.T) {
201+
sc := &ScriptConfig{
202+
Kind: "sh",
203+
Run: "base.sh",
204+
Env: map[string]string{"A": "base", "B": "base"},
205+
Posix: &ScriptConfig{
206+
Env: map[string]string{"B": "override", "C": "new"},
207+
},
208+
Windows: &ScriptConfig{
209+
Env: map[string]string{"B": "win-override", "D": "win-new"},
210+
},
211+
}
212+
213+
applyPlatformOverride(sc)
214+
215+
require.Equal(t, "base", sc.Env["A"], "base env preserved")
216+
// Depending on platform, B should be overridden
217+
require.NotEmpty(t, sc.Env["B"])
218+
}
219+
180220
func createFile(t *testing.T, base, relPath, content string) {
181221
t.Helper()
182222
fullPath := filepath.Join(base, relPath)

cli/azd/extensions/microsoft.azd.scripts/internal/provisioning/executor.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,6 @@ import (
1616
// ScriptResult holds the outcome of a single script execution.
1717
type ScriptResult struct {
1818
ExitCode int
19-
Stdout string
20-
Stderr string
2119
}
2220

2321
// ScriptExecutor runs shell scripts with a prepared environment.
@@ -79,13 +77,15 @@ func pwshBinary() string {
7977

8078
func shBinary() string {
8179
if runtime.GOOS == "windows" {
82-
// Prefer Git Bash on Windows
8380
if gitBash, err := exec.LookPath("bash.exe"); err == nil {
8481
return gitBash
8582
}
8683
return "bash.exe"
8784
}
88-
return "/bin/bash"
85+
if bash, err := exec.LookPath("bash"); err == nil {
86+
return bash
87+
}
88+
return "/bin/sh"
8989
}
9090

9191
// mapToEnvSlice converts a map to the KEY=VALUE slice format expected by exec.Cmd.Env.
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
package provisioning
5+
6+
import (
7+
"testing"
8+
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
func TestBuildShellCommand_Sh(t *testing.T) {
13+
shell, args := buildShellCommand("sh", "/path/to/script.sh")
14+
require.NotEmpty(t, shell)
15+
require.Contains(t, args, "/path/to/script.sh")
16+
}
17+
18+
func TestBuildShellCommand_Pwsh(t *testing.T) {
19+
shell, args := buildShellCommand("pwsh", "/path/to/script.ps1")
20+
require.Contains(t, shell, "pwsh")
21+
require.Contains(t, args, "-NoProfile")
22+
require.Contains(t, args, "-NonInteractive")
23+
require.Contains(t, args, "-File")
24+
require.Contains(t, args, "/path/to/script.ps1")
25+
}
26+
27+
func TestMapToEnvSlice(t *testing.T) {
28+
env := map[string]string{
29+
"FOO": "bar",
30+
"BAZ": "qux",
31+
}
32+
33+
slice := mapToEnvSlice(env)
34+
require.Len(t, slice, 2)
35+
require.Contains(t, slice, "FOO=bar")
36+
require.Contains(t, slice, "BAZ=qux")
37+
}
38+
39+
func TestMapToEnvSlice_Empty(t *testing.T) {
40+
slice := mapToEnvSlice(map[string]string{})
41+
require.Empty(t, slice)
42+
}

cli/azd/extensions/microsoft.azd.scripts/internal/provisioning/output_collector.go

Lines changed: 16 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import (
99
"maps"
1010
"os"
1111
"path/filepath"
12-
"strings"
1312
)
1413

1514
// OutputParameter represents a single output value from a script.
@@ -28,17 +27,32 @@ func NewOutputCollector(projectPath string) *OutputCollector {
2827
return &OutputCollector{projectPath: projectPath}
2928
}
3029

30+
// maxOutputFileSize is the maximum allowed size for an outputs.json file (10 MB).
31+
const maxOutputFileSize = 10 * 1024 * 1024
32+
3133
// Collect looks for an outputs.json file in the same directory as the script
3234
// and parses it into a map of output parameters. If no file is found, returns an empty map.
3335
func (c *OutputCollector) Collect(sc *ScriptConfig) (map[string]OutputParameter, error) {
3436
scriptDir := filepath.Dir(filepath.Join(c.projectPath, sc.Run))
3537
outputsPath := filepath.Join(scriptDir, "outputs.json")
3638

37-
data, err := os.ReadFile(outputsPath)
39+
fi, err := os.Stat(outputsPath)
3840
if err != nil {
3941
if os.IsNotExist(err) {
4042
return nil, nil
4143
}
44+
return nil, fmt.Errorf("checking outputs file %q: %w", outputsPath, err)
45+
}
46+
47+
if fi.Size() > maxOutputFileSize {
48+
return nil, fmt.Errorf(
49+
"outputs file %q exceeds maximum allowed size of %d bytes (actual: %d bytes)",
50+
outputsPath, maxOutputFileSize, fi.Size(),
51+
)
52+
}
53+
54+
data, err := os.ReadFile(outputsPath)
55+
if err != nil {
4256
return nil, fmt.Errorf("reading outputs file %q: %w", outputsPath, err)
4357
}
4458

@@ -59,23 +73,3 @@ func MergeOutputs(outputs ...map[string]OutputParameter) map[string]OutputParame
5973
}
6074
return merged
6175
}
62-
63-
// OutputsToEnvMap converts output parameters to a flat key=value map
64-
// suitable for storing in the azd environment.
65-
func OutputsToEnvMap(outputs map[string]OutputParameter) map[string]string {
66-
result := make(map[string]string, len(outputs))
67-
for k, v := range outputs {
68-
result[k] = v.Value
69-
}
70-
return result
71-
}
72-
73-
// OutputsToProvisioning converts output parameters to the provisioning output format
74-
// with uppercase keys following azd conventions.
75-
func OutputsToProvisioning(outputs map[string]OutputParameter) map[string]OutputParameter {
76-
result := make(map[string]OutputParameter, len(outputs))
77-
for k, v := range outputs {
78-
result[strings.ToUpper(k)] = v
79-
}
80-
return result
81-
}

cli/azd/extensions/microsoft.azd.scripts/internal/provisioning/output_collector_test.go

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -60,14 +60,3 @@ func TestMergeOutputs(t *testing.T) {
6060
require.Equal(t, "b", merged["X"].Value, "later overrides earlier")
6161
require.Equal(t, "c", merged["Y"].Value)
6262
}
63-
64-
func TestOutputsToEnvMap(t *testing.T) {
65-
outputs := map[string]OutputParameter{
66-
"KEY1": {Type: "string", Value: "val1"},
67-
"KEY2": {Type: "string", Value: "val2"},
68-
}
69-
70-
env := OutputsToEnvMap(outputs)
71-
require.Equal(t, "val1", env["KEY1"])
72-
require.Equal(t, "val2", env["KEY2"])
73-
}

cli/azd/extensions/microsoft.azd.scripts/internal/provisioning/provider.go

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package provisioning
66
import (
77
"context"
88
"fmt"
9+
"os"
910

1011
"github.com/azure/azure-dev/cli/azd/pkg/azdext"
1112
"github.com/azure/azure-dev/cli/azd/pkg/grpcbroker"
@@ -58,17 +59,9 @@ func (p *ScriptProvisioningProvider) State(
5859
ctx context.Context,
5960
options *azdext.ProvisioningStateOptions,
6061
) (*azdext.ProvisioningStateResult, error) {
61-
protoOutputs := make(map[string]*azdext.ProvisioningOutputParameter, len(p.outputs))
62-
for k, v := range p.outputs {
63-
protoOutputs[k] = &azdext.ProvisioningOutputParameter{
64-
Type: v.Type,
65-
Value: v.Value,
66-
}
67-
}
68-
6962
return &azdext.ProvisioningStateResult{
7063
State: &azdext.ProvisioningState{
71-
Outputs: protoOutputs,
64+
Outputs: toProtoOutputs(p.outputs),
7265
},
7366
}, nil
7467
}
@@ -89,17 +82,9 @@ func (p *ScriptProvisioningProvider) Deploy(
8982

9083
p.outputs = MergeOutputs(p.outputs, outputs)
9184

92-
protoOutputs := make(map[string]*azdext.ProvisioningOutputParameter, len(p.outputs))
93-
for k, v := range p.outputs {
94-
protoOutputs[k] = &azdext.ProvisioningOutputParameter{
95-
Type: v.Type,
96-
Value: v.Value,
97-
}
98-
}
99-
10085
return &azdext.ProvisioningDeployResult{
10186
Deployment: &azdext.ProvisioningDeployment{
102-
Outputs: protoOutputs,
87+
Outputs: toProtoOutputs(p.outputs),
10388
},
10489
}, nil
10590
}
@@ -206,11 +191,26 @@ func (p *ScriptProvisioningProvider) runScripts(
206191
return allOutputs, nil
207192
}
208193

194+
// toProtoOutputs converts internal OutputParameter map to the protobuf format.
195+
func toProtoOutputs(outputs map[string]OutputParameter) map[string]*azdext.ProvisioningOutputParameter {
196+
result := make(map[string]*azdext.ProvisioningOutputParameter, len(outputs))
197+
for k, v := range outputs {
198+
result[k] = &azdext.ProvisioningOutputParameter{Type: v.Type, Value: v.Value}
199+
}
200+
return result
201+
}
202+
209203
// getAzdEnv retrieves current azd environment values via the gRPC client.
210204
func (p *ScriptProvisioningProvider) getAzdEnv(ctx context.Context) (map[string]string, error) {
211205
resp, err := p.azdClient.Environment().GetValues(ctx, &azdext.GetEnvironmentRequest{})
212206
if err != nil {
213-
// If environment is not available, return empty map
207+
// Context cancellation should propagate
208+
if ctx.Err() != nil {
209+
return nil, ctx.Err()
210+
}
211+
212+
// Log the error but continue with empty env — environment may not be initialized yet
213+
fmt.Fprintf(os.Stderr, "Warning: could not load azd environment: %v\n", err)
214214
return make(map[string]string), nil
215215
}
216216

0 commit comments

Comments
 (0)