Skip to content

Commit 100189f

Browse files
weikanglimCopilot
andauthored
feat: support hooks per provisioning layer (#7382)
* add hooks support * test: refine layer hooks functional smoke tests Switch the hooks sample to a lightweight App Service app, verify deploy via command and prerestore hooks, and keep `azd hooks run` coverage in local-only subtests so the smoke stays fast. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * update schema * simplify tests * fix Clean() on abs path * fix test on Windows * 2nd round fixes * Fix hook validation cwd handling and provider validation errors Validate hooks using scope-specific working directories so relative file hooks are resolved correctly for services and layers, remove the redundant layer hook conversion during provision, and clean up provisioning validation error wrapping so root and layer errors are wrapped consistently. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 751a3e7 commit 100189f

21 files changed

Lines changed: 1213 additions & 143 deletions

File tree

cli/azd/cmd/hooks.go

Lines changed: 71 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -54,14 +54,15 @@ func newHooksRunFlags(cmd *cobra.Command, global *internal.GlobalCommandOptions)
5454
func newHooksRunCmd() *cobra.Command {
5555
return &cobra.Command{
5656
Use: "run <name>",
57-
Short: "Runs the specified hook for the project and services",
57+
Short: "Runs the specified hook for the project, provisioning layers, and services",
5858
Args: cobra.ExactArgs(1),
5959
}
6060
}
6161

6262
type hooksRunFlags struct {
6363
internal.EnvFlag
6464
global *internal.GlobalCommandOptions
65+
layer string
6566
platform string
6667
service string
6768
}
@@ -70,6 +71,7 @@ func (f *hooksRunFlags) Bind(local *pflag.FlagSet, global *internal.GlobalComman
7071
f.EnvFlag.Bind(local, global)
7172
f.global = global
7273

74+
local.StringVar(&f.layer, "layer", "", "Only runs hooks for the specified provisioning layer.")
7375
local.StringVar(&f.platform, "platform", "", "Forces hooks to run for the specified platform.")
7476
local.StringVar(&f.service, "service", "", "Only runs hooks for the specified service.")
7577
}
@@ -114,6 +116,7 @@ type hookContextType string
114116

115117
const (
116118
hookContextProject hookContextType = "command"
119+
hookContextLayer hookContextType = "layer"
117120
hookContextService hookContextType = "service"
118121
)
119122

@@ -142,8 +145,20 @@ var knownHookNames = map[string]bool{
142145
func (hra *hooksRunAction) Run(ctx context.Context) (*actions.ActionResult, error) {
143146
hookName := hra.args[0]
144147

148+
if hra.flags.service != "" && hra.flags.layer != "" {
149+
return nil,
150+
151+
&internal.ErrorWithSuggestion{
152+
Err: fmt.Errorf(
153+
"--service and --layer cannot be used together: %w", internal.ErrInvalidFlagCombination),
154+
Suggestion: "Choose either '--service' to run service hooks or '--layer' to run provisioning layer hooks.",
155+
}
156+
}
157+
145158
hookType := "project"
146-
if hra.flags.service != "" {
159+
if hra.flags.layer != "" {
160+
hookType = "layer"
161+
} else if hra.flags.service != "" {
147162
hookType = "service"
148163
}
149164

@@ -184,6 +199,12 @@ func (hra *hooksRunAction) Run(ctx context.Context) (*actions.ActionResult, erro
184199
}
185200
}
186201

202+
if hra.flags.layer != "" {
203+
if _, err := hra.projectConfig.Infra.GetLayer(hra.flags.layer); err != nil {
204+
return nil, err
205+
}
206+
}
207+
187208
// Project level hooks
188209
projectHooks := hra.projectConfig.Hooks[hookName]
189210

@@ -204,6 +225,24 @@ func (hra *hooksRunAction) Run(ctx context.Context) (*actions.ActionResult, erro
204225
return nil, err
205226
}
206227

228+
for _, layer := range hra.projectConfig.Infra.Layers {
229+
layerPath := layer.AbsolutePath(hra.projectConfig.Path)
230+
231+
skip := hra.flags.layer != "" && layer.Name != hra.flags.layer
232+
233+
hra.console.Message(ctx, "\n"+output.WithHighLightFormat(fmt.Sprintf("Layer: %s", layer.Name)))
234+
if err := hra.processHooks(
235+
ctx,
236+
layerPath,
237+
hookName,
238+
layer.Hooks[hookName],
239+
hookContextLayer,
240+
skip,
241+
); err != nil {
242+
return nil, err
243+
}
244+
}
245+
207246
// Service level hooks
208247
for _, service := range stableServices {
209248
serviceHooks := service.Hooks[hookName]
@@ -212,7 +251,7 @@ func (hra *hooksRunAction) Run(ctx context.Context) (*actions.ActionResult, erro
212251
hra.console.Message(ctx, "\n"+output.WithHighLightFormat(service.Name))
213252
if err := hra.processHooks(
214253
ctx,
215-
service.RelativePath,
254+
service.Path(),
216255
hookName,
217256
serviceHooks,
218257
hookContextService,
@@ -246,7 +285,7 @@ func (hra *hooksRunAction) processHooks(
246285
// When skipping, show individual skip messages for each hook that would have run
247286
for i := range hooks {
248287
hra.console.MessageUxItem(ctx, &ux.SkippedMessage{
249-
Message: fmt.Sprintf("service hook %d/%d", i+1, len(hooks)),
288+
Message: fmt.Sprintf("%s hook %d/%d", contextType, i+1, len(hooks)),
250289
})
251290
}
252291

@@ -312,37 +351,43 @@ func (hra *hooksRunAction) execHook(
312351

313352
// Validates hooks and displays warnings for default shell usage and other issues
314353
func (hra *hooksRunAction) validateAndWarnHooks(ctx context.Context) error {
315-
// Collect all hooks from project and services
316-
allHooks := make(map[string][]*ext.HookConfig)
354+
warningKeys := map[string]struct{}{}
355+
validateAndWarn := func(cwd string, hooks map[string][]*ext.HookConfig) {
356+
if len(hooks) == 0 {
357+
return
358+
}
317359

318-
// Add project hooks
319-
for hookName, hookConfigs := range hra.projectConfig.Hooks {
320-
allHooks[hookName] = append(allHooks[hookName], hookConfigs...)
360+
hooksManager := ext.NewHooksManager(cwd, hra.commandRunner)
361+
validationResult := hooksManager.ValidateHooks(ctx, hooks)
362+
363+
for _, warning := range validationResult.Warnings {
364+
key := warning.Message + "\x00" + warning.Suggestion
365+
if _, has := warningKeys[key]; has {
366+
continue
367+
}
368+
369+
warningKeys[key] = struct{}{}
370+
hra.console.MessageUxItem(ctx, &ux.WarningMessage{
371+
Description: warning.Message,
372+
})
373+
if warning.Suggestion != "" {
374+
hra.console.Message(ctx, warning.Suggestion)
375+
}
376+
hra.console.Message(ctx, "")
377+
}
321378
}
322379

323-
// Add service hooks
380+
validateAndWarn(hra.projectConfig.Path, hra.projectConfig.Hooks)
381+
324382
stableServices, err := hra.importManager.ServiceStable(ctx, hra.projectConfig)
325383
if err == nil {
326384
for _, service := range stableServices {
327-
for hookName, hookConfigs := range service.Hooks {
328-
allHooks[hookName] = append(allHooks[hookName], hookConfigs...)
329-
}
385+
validateAndWarn(service.Path(), service.Hooks)
330386
}
331387
}
332388

333-
// Create hooks manager and validate
334-
hooksManager := ext.NewHooksManager(hra.projectConfig.Path, hra.commandRunner)
335-
validationResult := hooksManager.ValidateHooks(ctx, allHooks)
336-
337-
// Display any warnings
338-
for _, warning := range validationResult.Warnings {
339-
hra.console.MessageUxItem(ctx, &ux.WarningMessage{
340-
Description: warning.Message,
341-
})
342-
if warning.Suggestion != "" {
343-
hra.console.Message(ctx, warning.Suggestion)
344-
}
345-
hra.console.Message(ctx, "")
389+
for _, layer := range hra.projectConfig.Infra.Layers {
390+
validateAndWarn(layer.AbsolutePath(hra.projectConfig.Path), layer.Hooks)
346391
}
347392

348393
return nil

0 commit comments

Comments
 (0)