From 592a44fb40c74e7c625d62f2623eea93721f457e Mon Sep 17 00:00:00 2001 From: izeau Date: Wed, 15 May 2024 23:39:42 +0200 Subject: [PATCH 1/9] feat: implement hooks --- .examples/hooks/hooks/post_scaffold | 4 ++ .examples/hooks/scaffold.yaml | 3 + .examples/hooks/{{ .ProjectKebab }}/file.txt | 1 + app/commands/cmd_new.go | 15 +++++ app/commands/controller.go | 69 ++++++++++++++++++++ app/commands/runner.go | 5 ++ app/core/rule/rule.go | 69 ++++++++++++++++++++ app/core/rwfs/mem.go | 4 ++ app/core/rwfs/os.go | 52 +++++++++++++++ app/core/rwfs/rwfs.go | 4 ++ app/scaffold/rc.go | 4 ++ docs/docs/.vitepress/config.mts | 1 + docs/docs/templates/hooks.md | 16 +++++ docs/docs/user-guide/scaffold-rc.md | 8 +++ main.go | 6 ++ tests/hooks-snapshot.test.sh | 15 +++++ tests/snapshots/hooks.snapshot.txt | 5 ++ 17 files changed, 281 insertions(+) create mode 100644 .examples/hooks/hooks/post_scaffold create mode 100644 .examples/hooks/scaffold.yaml create mode 100644 .examples/hooks/{{ .ProjectKebab }}/file.txt create mode 100644 app/core/rule/rule.go create mode 100644 docs/docs/templates/hooks.md create mode 100755 tests/hooks-snapshot.test.sh create mode 100644 tests/snapshots/hooks.snapshot.txt diff --git a/.examples/hooks/hooks/post_scaffold b/.examples/hooks/hooks/post_scaffold new file mode 100644 index 0000000..a091260 --- /dev/null +++ b/.examples/hooks/hooks/post_scaffold @@ -0,0 +1,4 @@ +#!/usr/bin/env python3 + +with open("{{ .ProjectKebab }}/file.txt", "a") as f: + f.write("Hello\n") diff --git a/.examples/hooks/scaffold.yaml b/.examples/hooks/scaffold.yaml new file mode 100644 index 0000000..489e9dd --- /dev/null +++ b/.examples/hooks/scaffold.yaml @@ -0,0 +1,3 @@ +presets: + default: + Project: "scaffold-test-default" diff --git a/.examples/hooks/{{ .ProjectKebab }}/file.txt b/.examples/hooks/{{ .ProjectKebab }}/file.txt new file mode 100644 index 0000000..6aa652f --- /dev/null +++ b/.examples/hooks/{{ .ProjectKebab }}/file.txt @@ -0,0 +1 @@ +Hook says: diff --git a/app/commands/cmd_new.go b/app/commands/cmd_new.go index b0edd8d..e27a07a 100644 --- a/app/commands/cmd_new.go +++ b/app/commands/cmd_new.go @@ -9,6 +9,7 @@ import ( "github.com/go-git/go-git/v5/plumbing/transport" "github.com/go-git/go-git/v5/plumbing/transport/http" "github.com/hay-kot/scaffold/app/core/fsast" + "github.com/hay-kot/scaffold/app/core/rule" "github.com/hay-kot/scaffold/app/scaffold" "github.com/hay-kot/scaffold/app/scaffold/pkgs" "github.com/hay-kot/scaffold/internal/styles" @@ -17,6 +18,7 @@ import ( type FlagsNew struct { NoPrompt bool + RunHooks string Preset string Snapshot string } @@ -76,6 +78,19 @@ func (ctrl *Controller) New(args []string, flags FlagsNew) error { } } + if flags.RunHooks != "inherit" { + runHooks, err := rule.NewFromString(flags.RunHooks) + if err != nil { + return err + } + + ctrl.runHooks = runHooks + } + + if ctrl.runHooks == rule.Prompt && flags.NoPrompt { + ctrl.runHooks = rule.No + } + outfs := ctrl.Flags.OutputFS() err = ctrl.runscaffold(runconf{ diff --git a/app/commands/controller.go b/app/commands/controller.go index e110e0f..cdf4b6e 100644 --- a/app/commands/controller.go +++ b/app/commands/controller.go @@ -2,7 +2,15 @@ package commands import ( + "errors" + "fmt" + "io/fs" + "os" + "path/filepath" + + "github.com/charmbracelet/huh" "github.com/hay-kot/scaffold/app/core/engine" + "github.com/hay-kot/scaffold/app/core/rule" "github.com/hay-kot/scaffold/app/core/rwfs" "github.com/hay-kot/scaffold/app/scaffold" ) @@ -32,6 +40,7 @@ type Controller struct { engine *engine.Engine rc *scaffold.ScaffoldRC + runHooks rule.Rule prepared bool } @@ -41,6 +50,33 @@ func (ctrl *Controller) Prepare(e *engine.Engine, src *scaffold.ScaffoldRC) { ctrl.engine = e ctrl.rc = src ctrl.prepared = true + ctrl.runHooks = src.RunHooks +} + +func (ctrl *Controller) RunHook(rfs rwfs.ReadFS, name string, wfs rwfs.WriteFS, vars any, args ...string) error { + src, err := fs.ReadFile(rfs, filepath.Join("hooks", name)) + if err != nil { + if !errors.Is(err, os.ErrNotExist) { + return fmt.Errorf("failed to open %q hook file: %w", name, err) + } + return nil + } + + rendered, err := ctrl.engine.TmplString(string(src), vars) + if err != nil { + return err + } + + if !mayRunHook(ctrl.runHooks, name, rendered) { + return nil + } + + err = wfs.RunHook(name, []byte(rendered), args) + if err != nil && !errors.Is(err, rwfs.ErrHooksNotSupported) { + return err + } + + return nil } func (ctrl *Controller) ready() { @@ -48,3 +84,36 @@ func (ctrl *Controller) ready() { panic("controller not prepared") } } + +func mayRunHook(hookRule rule.Rule, name string, rendered string) bool { + for { + switch hookRule { + case rule.Unset: + return true + case rule.Yes: + return true + case rule.No: + return false + case rule.Prompt: + } + + err := huh.Run(huh.NewSelect[rule.Rule](). + Title(fmt.Sprintf("scaffold defines a %s hook", name)). + Options( + huh.NewOption("run", rule.Yes), + huh.NewOption("skip", rule.No), + huh.NewOption("review", rule.Prompt)). + Value(&hookRule)) + + if err != nil { + fmt.Fprint(os.Stderr, err) + return false + } + + if hookRule != rule.Prompt { + continue + } + + fmt.Printf("\n%s\n", rendered) + } +} diff --git a/app/commands/runner.go b/app/commands/runner.go index 67922db..eb5a6d4 100644 --- a/app/commands/runner.go +++ b/app/commands/runner.go @@ -63,6 +63,11 @@ func (ctrl *Controller) runscaffold(cfg runconf) error { return err } + err = ctrl.RunHook(pfs, "post_scaffold", cfg.outputfs, vars) + if err != nil { + return err + } + if cfg.showMessages && p.Conf.Messages.Post != "" { rendered, err := ctrl.engine.TmplString(p.Conf.Messages.Post, vars) if err != nil { diff --git a/app/core/rule/rule.go b/app/core/rule/rule.go new file mode 100644 index 0000000..d49635a --- /dev/null +++ b/app/core/rule/rule.go @@ -0,0 +1,69 @@ +// Package rule provides a straightforward and flexible way to handle rule-based +// logic. +package rule + +import ( + "fmt" + + "gopkg.in/yaml.v3" +) + +type Rule int + +const ( + Unset Rule = iota + Yes + No + Prompt +) + +func NewFromString(s string) (Rule, error) { + switch s { + case "yes": + return Yes, nil + case "no": + return No, nil + case "prompt": + return Prompt, nil + } + + return Unset, fmt.Errorf("invalid rule: %v", s) +} + +func (r Rule) String() string { + switch r { + case Unset: + return "unset" + case Yes: + return "yes" + case No: + return "no" + case Prompt: + return "prompt" + } + panic("invalid rule") +} + +func (r *Rule) UnmarshalYAML(node *yaml.Node) error { + var asBool bool + var asString string + + switch { + case node.Decode(&asBool) == nil: + if asBool { + *r = Yes + } else { + *r = No + } + return nil + case node.Decode(&asString) == nil: + rule, err := NewFromString(asString) + if err != nil { + return err + } + *r = rule + return nil + default: + return fmt.Errorf("invalid rule: %v", node.Value) + } +} diff --git a/app/core/rwfs/mem.go b/app/core/rwfs/mem.go index fa43a14..6c54c20 100644 --- a/app/core/rwfs/mem.go +++ b/app/core/rwfs/mem.go @@ -34,6 +34,10 @@ func (m *MemoryWFS) WriteFile(path string, data []byte, perm fs.FileMode) error return m.FS.WriteFile(path, data, perm) } +func (m *MemoryWFS) RunHook(name string, data []byte, args []string) error { + return ErrHooksNotSupported +} + func NewMemoryWFS() *MemoryWFS { return &MemoryWFS{ FS: memfs.New(), diff --git a/app/core/rwfs/os.go b/app/core/rwfs/os.go index d52d34b..aec890f 100644 --- a/app/core/rwfs/os.go +++ b/app/core/rwfs/os.go @@ -1,8 +1,11 @@ package rwfs import ( + "context" "io/fs" "os" + "os/exec" + "os/signal" "path/filepath" ) @@ -34,3 +37,52 @@ func (o *OsWFS) MkdirAll(path string, perm fs.FileMode) error { func (o *OsWFS) WriteFile(name string, data []byte, perm fs.FileMode) error { return os.WriteFile(filepath.Join(o.root, name), data, perm) } + +func (o *OsWFS) RunHook(name string, data []byte, args []string) error { + tmp, err := writeHook(name, data) + + defer func() { + if rerr := os.Remove(tmp); rerr != nil && err == nil { + err = rerr + } + }() + + if err != nil { + return err + } + + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) + defer stop() + go func() { + // stop receiving signal notifications as soon as possible. + <-ctx.Done() + stop() + }() + + cmd := exec.CommandContext(ctx, tmp, append([]string{tmp}, args...)...) + cmd.Dir = o.root + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + err = cmd.Run() + return err +} + +func writeHook(name string, data []byte) (string, error) { + f, err := os.CreateTemp("", name) + if err != nil { + return "", err + } + + tmp := f.Name() + + err = os.Chmod(tmp, 0700) + if err != nil { + return tmp, err + } + + _, err = f.Write(data) + if cerr := f.Close(); cerr != nil && err == nil { + err = cerr + } + return tmp, err +} diff --git a/app/core/rwfs/rwfs.go b/app/core/rwfs/rwfs.go index c9bbbf3..a12d42c 100644 --- a/app/core/rwfs/rwfs.go +++ b/app/core/rwfs/rwfs.go @@ -3,9 +3,12 @@ package rwfs import ( + "errors" "io/fs" ) +var ErrHooksNotSupported = errors.New("hooks not supported") + // ReadFS is a read only file system that can be used to read files from // a file system. It is a alias for fs.FS. type ReadFS = fs.FS @@ -16,4 +19,5 @@ type WriteFS interface { fs.FS MkdirAll(path string, perm fs.FileMode) error WriteFile(name string, data []byte, perm fs.FileMode) error + RunHook(name string, data []byte, args []string) error } diff --git a/app/scaffold/rc.go b/app/scaffold/rc.go index 1079a78..7c0dfb0 100644 --- a/app/scaffold/rc.go +++ b/app/scaffold/rc.go @@ -12,6 +12,7 @@ import ( "github.com/go-git/go-git/v5/plumbing/transport" githttp "github.com/go-git/go-git/v5/plumbing/transport/http" + "github.com/hay-kot/scaffold/app/core/rule" "github.com/hay-kot/scaffold/internal/styles" "gopkg.in/yaml.v3" ) @@ -47,6 +48,9 @@ type ScaffoldRC struct { // Auth defines a list of auth entries that can be used to // authenticate with a remote SCM. Auth []AuthEntry `yaml:"auth"` + + // RunHooks defines the behavior when a scaffold defines hooks. + RunHooks rule.Rule `yaml:"run_hooks"` } type Settings struct { diff --git a/docs/docs/.vitepress/config.mts b/docs/docs/.vitepress/config.mts index 75f224f..4a36dcb 100644 --- a/docs/docs/.vitepress/config.mts +++ b/docs/docs/.vitepress/config.mts @@ -56,6 +56,7 @@ export default withMermaid( text: "File Reference", link: "/templates/config-reference", }, + { text: "Hooks", link: "/templates/hooks" }, { text: "Testing Scaffolds", link: "/templates/testing-scaffolds", diff --git a/docs/docs/templates/hooks.md b/docs/docs/templates/hooks.md new file mode 100644 index 0000000..2f10de3 --- /dev/null +++ b/docs/docs/templates/hooks.md @@ -0,0 +1,16 @@ +--- +--- + +# Hooks + +Hooks are extensionless files that are stored in the `hooks` subdirectory of your scaffold. They allow you to run scripts at specific points during project generation. They are skipped when the scaffold output directory is an in-memory filesystem or when they are explicitely disabled. The [shebang]() is mandatory and can be set to any interpreter on your system. Template variables are available in the scripts. + +Currently, only the `post_scaffold` hook is implemented. + +::: tip Working directory +The scripts' working directory is set to the scaffold output directory. +::: + +## `post_scaffold` + +The `post_scaffold` hook is executed after the files have been rendered on the disk, but before the `post` message is printed. It is typically used to fix the formatting of generated files. diff --git a/docs/docs/user-guide/scaffold-rc.md b/docs/docs/user-guide/scaffold-rc.md index 5ce6e1f..ffc5a20 100644 --- a/docs/docs/user-guide/scaffold-rc.md +++ b/docs/docs/user-guide/scaffold-rc.md @@ -99,3 +99,11 @@ auth: ::: tip the `match` key supports regular expressions giving you a lot of flexibility in defining your matchers. ::: + +## `run_hooks` + +You may disable hooks globally by setting `run_hooks` to `false`, or choose to be prompted before they run by setting it to `prompt`. The `--run-hooks` CLI setting takes precedence. + +```yaml +run_hooks: prompt +``` diff --git a/main.go b/main.go index fe354fc..ed7238e 100644 --- a/main.go +++ b/main.go @@ -187,6 +187,11 @@ func main() { Usage: "disable interactive mode", Value: false, }, + &cli.StringFlag{ + Name: "run-hooks", + Usage: "run hooks (yes, no, prompt, inherit; default: inherited from scaffoldrc)", + Value: "inherit", + }, &cli.StringFlag{ Name: "preset", Usage: "preset to use for the scaffold", @@ -201,6 +206,7 @@ func main() { Action: func(ctx *cli.Context) error { return ctrl.New(ctx.Args().Slice(), commands.FlagsNew{ NoPrompt: ctx.Bool("no-prompt"), + RunHooks: ctx.String("run-hooks"), Preset: ctx.String("preset"), Snapshot: ctx.String("snapshot"), }) diff --git a/tests/hooks-snapshot.test.sh b/tests/hooks-snapshot.test.sh new file mode 100755 index 0000000..26ed03d --- /dev/null +++ b/tests/hooks-snapshot.test.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +# Source the assert_snapshot function +source tests/assert.sh + +# Your script continues as before... +output=$($1 --log-level="error" \ + new \ + --preset="default" \ + --no-prompt \ + --snapshot="stdout" \ + hooks) + +# Call the function to assert the snapshot +assert_snapshot "hooks.snapshot.txt" "$output" diff --git a/tests/snapshots/hooks.snapshot.txt b/tests/snapshots/hooks.snapshot.txt new file mode 100644 index 0000000..a11c0f6 --- /dev/null +++ b/tests/snapshots/hooks.snapshot.txt @@ -0,0 +1,5 @@ +scaffold-test-default: (type=dir) + file.txt: (type=file) + Hook says: + Hello + From c142cfaa9b0744044afd313109e51e5ed16a0699 Mon Sep 17 00:00:00 2001 From: Hayden <64056131+hay-kot@users.noreply.github.com> Date: Sun, 2 Jun 2024 12:47:23 -0500 Subject: [PATCH 2/9] restructure config/options for hooks --- app/commands/cmd_new.go | 23 +----- app/commands/controller.go | 69 ----------------- app/commands/runner.go | 112 ++++++++++++++++++++++++++-- app/core/rule/rule.go | 69 ----------------- app/scaffold/project.go | 5 ++ app/scaffold/rc.go | 59 +++++++++++++-- docs/docs/user-guide/scaffold-rc.md | 23 ++++-- main.go | 14 ++-- tests/hooks-snapshot.test.sh | 1 + 9 files changed, 191 insertions(+), 184 deletions(-) delete mode 100644 app/core/rule/rule.go diff --git a/app/commands/cmd_new.go b/app/commands/cmd_new.go index e27a07a..2a5dd50 100644 --- a/app/commands/cmd_new.go +++ b/app/commands/cmd_new.go @@ -9,7 +9,6 @@ import ( "github.com/go-git/go-git/v5/plumbing/transport" "github.com/go-git/go-git/v5/plumbing/transport/http" "github.com/hay-kot/scaffold/app/core/fsast" - "github.com/hay-kot/scaffold/app/core/rule" "github.com/hay-kot/scaffold/app/scaffold" "github.com/hay-kot/scaffold/app/scaffold/pkgs" "github.com/hay-kot/scaffold/internal/styles" @@ -18,7 +17,6 @@ import ( type FlagsNew struct { NoPrompt bool - RunHooks string Preset string Snapshot string } @@ -78,26 +76,13 @@ func (ctrl *Controller) New(args []string, flags FlagsNew) error { } } - if flags.RunHooks != "inherit" { - runHooks, err := rule.NewFromString(flags.RunHooks) - if err != nil { - return err - } - - ctrl.runHooks = runHooks - } - - if ctrl.runHooks == rule.Prompt && flags.NoPrompt { - ctrl.runHooks = rule.No - } - outfs := ctrl.Flags.OutputFS() err = ctrl.runscaffold(runconf{ - scaffolddir: path, - showMessages: !flags.NoPrompt, - varfunc: varfunc, - outputfs: outfs, + scaffolddir: path, + noPrompt: flags.NoPrompt, + varfunc: varfunc, + outputfs: outfs, }) if err != nil { return err diff --git a/app/commands/controller.go b/app/commands/controller.go index cdf4b6e..e110e0f 100644 --- a/app/commands/controller.go +++ b/app/commands/controller.go @@ -2,15 +2,7 @@ package commands import ( - "errors" - "fmt" - "io/fs" - "os" - "path/filepath" - - "github.com/charmbracelet/huh" "github.com/hay-kot/scaffold/app/core/engine" - "github.com/hay-kot/scaffold/app/core/rule" "github.com/hay-kot/scaffold/app/core/rwfs" "github.com/hay-kot/scaffold/app/scaffold" ) @@ -40,7 +32,6 @@ type Controller struct { engine *engine.Engine rc *scaffold.ScaffoldRC - runHooks rule.Rule prepared bool } @@ -50,33 +41,6 @@ func (ctrl *Controller) Prepare(e *engine.Engine, src *scaffold.ScaffoldRC) { ctrl.engine = e ctrl.rc = src ctrl.prepared = true - ctrl.runHooks = src.RunHooks -} - -func (ctrl *Controller) RunHook(rfs rwfs.ReadFS, name string, wfs rwfs.WriteFS, vars any, args ...string) error { - src, err := fs.ReadFile(rfs, filepath.Join("hooks", name)) - if err != nil { - if !errors.Is(err, os.ErrNotExist) { - return fmt.Errorf("failed to open %q hook file: %w", name, err) - } - return nil - } - - rendered, err := ctrl.engine.TmplString(string(src), vars) - if err != nil { - return err - } - - if !mayRunHook(ctrl.runHooks, name, rendered) { - return nil - } - - err = wfs.RunHook(name, []byte(rendered), args) - if err != nil && !errors.Is(err, rwfs.ErrHooksNotSupported) { - return err - } - - return nil } func (ctrl *Controller) ready() { @@ -84,36 +48,3 @@ func (ctrl *Controller) ready() { panic("controller not prepared") } } - -func mayRunHook(hookRule rule.Rule, name string, rendered string) bool { - for { - switch hookRule { - case rule.Unset: - return true - case rule.Yes: - return true - case rule.No: - return false - case rule.Prompt: - } - - err := huh.Run(huh.NewSelect[rule.Rule](). - Title(fmt.Sprintf("scaffold defines a %s hook", name)). - Options( - huh.NewOption("run", rule.Yes), - huh.NewOption("skip", rule.No), - huh.NewOption("review", rule.Prompt)). - Value(&hookRule)) - - if err != nil { - fmt.Fprint(os.Stderr, err) - return false - } - - if hookRule != rule.Prompt { - continue - } - - fmt.Printf("\n%s\n", rendered) - } -} diff --git a/app/commands/runner.go b/app/commands/runner.go index eb5a6d4..c990c11 100644 --- a/app/commands/runner.go +++ b/app/commands/runner.go @@ -1,10 +1,15 @@ package commands import ( + "errors" "fmt" + "io/fs" "os" + "path/filepath" + "strings" "github.com/charmbracelet/glamour" + "github.com/charmbracelet/huh" "github.com/hay-kot/scaffold/app/core/rwfs" "github.com/hay-kot/scaffold/app/scaffold" ) @@ -12,8 +17,8 @@ import ( type runconf struct { // os path to the scaffold directory. scaffolddir string - // showMessages is a flag to show pre/post messages. - showMessages bool + // noPrompt is a flag to show pre/post messages. + noPrompt bool // varfunc is a function that returns a map of variables that is provided // to the template engine. varfunc func(*scaffold.Project) (map[string]any, error) @@ -25,15 +30,15 @@ type runconf struct { // so that we can allow the `test` and `new` commands to share as much of the same code // as possible. func (ctrl *Controller) runscaffold(cfg runconf) error { - pfs := os.DirFS(cfg.scaffolddir) - p, err := scaffold.LoadProject(pfs, scaffold.Options{ + scaffoldFS := os.DirFS(cfg.scaffolddir) + p, err := scaffold.LoadProject(scaffoldFS, scaffold.Options{ NoClobber: ctrl.Flags.NoClobber, }) if err != nil { return err } - if cfg.showMessages && p.Conf.Messages.Pre != "" { + if !cfg.noPrompt && p.Conf.Messages.Pre != "" { out, err := glamour.RenderWithEnvironmentConfig(p.Conf.Messages.Pre) if err != nil { return err @@ -49,7 +54,7 @@ func (ctrl *Controller) runscaffold(cfg runconf) error { args := &scaffold.RWFSArgs{ Project: p, - ReadFS: pfs, + ReadFS: scaffoldFS, WriteFS: cfg.outputfs, } @@ -63,12 +68,15 @@ func (ctrl *Controller) runscaffold(cfg runconf) error { return err } - err = ctrl.RunHook(pfs, "post_scaffold", cfg.outputfs, vars) + if ctrl.rc.Settings.RunHooks != scaffold.RunHooksNever { + } + + err = ctrl.runHook(scaffoldFS, cfg.outputfs, scaffold.PostScaffoldScripts, vars, cfg.noPrompt) if err != nil { return err } - if cfg.showMessages && p.Conf.Messages.Post != "" { + if !cfg.noPrompt && p.Conf.Messages.Post != "" { rendered, err := ctrl.engine.TmplString(p.Conf.Messages.Post, vars) if err != nil { return err @@ -84,3 +92,91 @@ func (ctrl *Controller) runscaffold(cfg runconf) error { return nil } + +func (ctrl *Controller) runHook( + rfs rwfs.ReadFS, + wfs rwfs.WriteFS, + hookPrefix string, + vars any, + noPrompt bool, +) error { + sources, err := fs.ReadDir(rfs, scaffold.HooksDir) + if err != nil { + if !errors.Is(err, os.ErrNotExist) { + return fmt.Errorf("failed to open %q hook file: %w", hookPrefix, err) + } + return nil + } + + // find first glob match + var hookContents []byte + for _, source := range sources { + if source.IsDir() { + continue + } + + if strings.HasPrefix(source.Name(), hookPrefix) { + path := filepath.Join(scaffold.HooksDir, source.Name()) + + hookContents, err = fs.ReadFile(rfs, path) + if err != nil { + return err + } + } + } + + if len(hookContents) == 0 { + return nil + } + + rendered, err := ctrl.engine.TmplString(string(hookContents), vars) + if err != nil { + return err + } + + if !shouldRunHooks(ctrl.rc.Settings.RunHooks, noPrompt, hookPrefix, rendered) { + return nil + } + + err = wfs.RunHook(hookPrefix, []byte(rendered), nil) + if err != nil && !errors.Is(err, rwfs.ErrHooksNotSupported) { + return err + } + + return nil +} + +// shouldRunHooks will resolve the users RunHooks preference and either return the preference +// or prompt the user for their choice when the preference is RunHooksPrompt +func shouldRunHooks(runPreference scaffold.RunHooksOption, noPrompt bool, name string, rendered string) bool { + for { + switch runPreference { + case scaffold.RunHooksAlways: + return true + case scaffold.RunHooksNever: + return false + case scaffold.RunHooksPrompt: + if noPrompt { + return false + } + + err := huh.Run(huh.NewSelect[scaffold.RunHooksOption](). + Title(fmt.Sprintf("scaffold defines a %s hook", name)). + Options( + huh.NewOption("run", scaffold.RunHooksAlways), + huh.NewOption("skip", scaffold.RunHooksNever), + huh.NewOption("review", scaffold.RunHooksPrompt)). + Value(&runPreference)) + if err != nil { + fmt.Fprint(os.Stderr, err) + return false + } + + if runPreference == scaffold.RunHooksPrompt { + fmt.Printf("\n%s\n", rendered) + } + default: + return false + } + } +} diff --git a/app/core/rule/rule.go b/app/core/rule/rule.go deleted file mode 100644 index d49635a..0000000 --- a/app/core/rule/rule.go +++ /dev/null @@ -1,69 +0,0 @@ -// Package rule provides a straightforward and flexible way to handle rule-based -// logic. -package rule - -import ( - "fmt" - - "gopkg.in/yaml.v3" -) - -type Rule int - -const ( - Unset Rule = iota - Yes - No - Prompt -) - -func NewFromString(s string) (Rule, error) { - switch s { - case "yes": - return Yes, nil - case "no": - return No, nil - case "prompt": - return Prompt, nil - } - - return Unset, fmt.Errorf("invalid rule: %v", s) -} - -func (r Rule) String() string { - switch r { - case Unset: - return "unset" - case Yes: - return "yes" - case No: - return "no" - case Prompt: - return "prompt" - } - panic("invalid rule") -} - -func (r *Rule) UnmarshalYAML(node *yaml.Node) error { - var asBool bool - var asString string - - switch { - case node.Decode(&asBool) == nil: - if asBool { - *r = Yes - } else { - *r = No - } - return nil - case node.Decode(&asString) == nil: - rule, err := NewFromString(asString) - if err != nil { - return err - } - *r = rule - return nil - default: - return fmt.Errorf("invalid rule: %v", node.Value) - } -} diff --git a/app/scaffold/project.go b/app/scaffold/project.go index a18cf9a..c2ac1c0 100644 --- a/app/scaffold/project.go +++ b/app/scaffold/project.go @@ -14,6 +14,11 @@ import ( "github.com/rs/zerolog/log" ) +const ( + HooksDir = "hooks" + PostScaffoldScripts = "post_scaffold" +) + var projectNames = [...]string{ "{{ .Project }}", "{{ .ProjectSlug }}", diff --git a/app/scaffold/rc.go b/app/scaffold/rc.go index 7c0dfb0..2b1783d 100644 --- a/app/scaffold/rc.go +++ b/app/scaffold/rc.go @@ -12,7 +12,6 @@ import ( "github.com/go-git/go-git/v5/plumbing/transport" githttp "github.com/go-git/go-git/v5/plumbing/transport/http" - "github.com/hay-kot/scaffold/app/core/rule" "github.com/hay-kot/scaffold/internal/styles" "gopkg.in/yaml.v3" ) @@ -48,13 +47,55 @@ type ScaffoldRC struct { // Auth defines a list of auth entries that can be used to // authenticate with a remote SCM. Auth []AuthEntry `yaml:"auth"` +} + +type RunHooksOption string + +var ( + RunHooksNever RunHooksOption = "never" + RunHooksAlways RunHooksOption = "always" + RunHooksPrompt RunHooksOption = "prompt" +) + +func ParseRunHooksOption(s string) RunHooksOption { + zero := RunHooksOption("") + ptr := &zero + _ = ptr.UnmarshalText([]byte(s)) + return *ptr +} + +func (r *RunHooksOption) UnmarshalText(text []byte) error { + switch string(text) { + case "never", "no", "false": + *r = RunHooksNever + case "always", "yes", "true": + *r = RunHooksAlways + case "prompt", "": // if left empty, default to prompt + *r = RunHooksPrompt + default: + // fallback to whatever they input so we can log the incorrect value + *r = RunHooksOption(string(text)) + } + + return nil +} + +func (r RunHooksOption) IsValid() bool { + switch r { + case RunHooksNever, RunHooksAlways, RunHooksPrompt: + return true + default: + return false + } +} - // RunHooks defines the behavior when a scaffold defines hooks. - RunHooks rule.Rule `yaml:"run_hooks"` +func (r RunHooksOption) String() string { + return string(r) } type Settings struct { - Theme styles.HuhTheme `yaml:"theme"` + Theme styles.HuhTheme `yaml:"theme"` + RunHooks RunHooksOption `yaml:"run_hooks"` } type AuthEntry struct { @@ -83,7 +124,8 @@ func (e RcValidationErrors) Error() string { func DefaultScaffoldRC() *ScaffoldRC { return &ScaffoldRC{ Settings: Settings{ - Theme: styles.HuhThemeScaffold, + Theme: styles.HuhThemeScaffold, + RunHooks: RunHooksPrompt, }, } } @@ -137,6 +179,13 @@ func (rc *ScaffoldRC) Validate() error { }) } + if !rc.Settings.RunHooks.IsValid() { + errs = append(errs, RCValidationError{ + Key: "settings.run_hooks", + Cause: fmt.Errorf("invalid run_hooks: %s", rc.Settings.RunHooks.String()), + }) + } + if len(errs) > 0 { return RcValidationErrors(errs) } diff --git a/docs/docs/user-guide/scaffold-rc.md b/docs/docs/user-guide/scaffold-rc.md index ffc5a20..696fbc6 100644 --- a/docs/docs/user-guide/scaffold-rc.md +++ b/docs/docs/user-guide/scaffold-rc.md @@ -31,6 +31,21 @@ The Theme settings allows the user to set the default theme for the scaffolding - `base16` - `catppuccino` +### `run_hooks` + +You may disable hooks globally by setting `run_hooks` to `never`, or choose to be prompted before they run by setting it to `prompt`. The `--run-hooks` CLI setting takes precedence. Options include: + +- `always` - run hooks without prompting +- `never` - never run hooks without prompting +- `prompt` - prompt before running hooks (default) + +**Example** + +```yaml +settings: + run_hooks: prompt +``` + ## `defaults` The `defaults` section allows you to set some default values for the scaffolding process. These can be any key/value string pairs @@ -99,11 +114,3 @@ auth: ::: tip the `match` key supports regular expressions giving you a lot of flexibility in defining your matchers. ::: - -## `run_hooks` - -You may disable hooks globally by setting `run_hooks` to `false`, or choose to be prompted before they run by setting it to `prompt`. The `--run-hooks` CLI setting takes precedence. - -```yaml -run_hooks: prompt -``` diff --git a/main.go b/main.go index ed7238e..a4ee0e4 100644 --- a/main.go +++ b/main.go @@ -98,6 +98,10 @@ func main() { Value: "scaffold", EnvVars: []string{"SCAFFOLD_THEME"}, }, + &cli.StringFlag{ + Name: "run-hooks", + Usage: "run hooks (never, always, prompt) when provided overrides scaffold rc", + }, }, Before: func(ctx *cli.Context) error { ctrl.Flags = commands.Flags{ @@ -156,6 +160,10 @@ func main() { rc.Settings.Theme = styles.HuhTheme(ctx.String("theme")) } + if ctx.IsSet("run-hooks") { + rc.Settings.RunHooks = scaffold.ParseRunHooksOption(ctx.String("run-hooks")) + } + // // Validate Runtime Config // @@ -187,11 +195,6 @@ func main() { Usage: "disable interactive mode", Value: false, }, - &cli.StringFlag{ - Name: "run-hooks", - Usage: "run hooks (yes, no, prompt, inherit; default: inherited from scaffoldrc)", - Value: "inherit", - }, &cli.StringFlag{ Name: "preset", Usage: "preset to use for the scaffold", @@ -206,7 +209,6 @@ func main() { Action: func(ctx *cli.Context) error { return ctrl.New(ctx.Args().Slice(), commands.FlagsNew{ NoPrompt: ctx.Bool("no-prompt"), - RunHooks: ctx.String("run-hooks"), Preset: ctx.String("preset"), Snapshot: ctx.String("snapshot"), }) diff --git a/tests/hooks-snapshot.test.sh b/tests/hooks-snapshot.test.sh index 26ed03d..8618b4d 100755 --- a/tests/hooks-snapshot.test.sh +++ b/tests/hooks-snapshot.test.sh @@ -5,6 +5,7 @@ source tests/assert.sh # Your script continues as before... output=$($1 --log-level="error" \ + --run-hooks="always" \ new \ --preset="default" \ --no-prompt \ From 8bb8990aedf2f45837c691b5db0ed55dec6b1314 Mon Sep 17 00:00:00 2001 From: Hayden <64056131+hay-kot@users.noreply.github.com> Date: Sun, 2 Jun 2024 12:49:36 -0500 Subject: [PATCH 3/9] guard run hooks to avoid unnecessary work when set to never run --- app/commands/runner.go | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/app/commands/runner.go b/app/commands/runner.go index c990c11..0c7b5eb 100644 --- a/app/commands/runner.go +++ b/app/commands/runner.go @@ -69,11 +69,10 @@ func (ctrl *Controller) runscaffold(cfg runconf) error { } if ctrl.rc.Settings.RunHooks != scaffold.RunHooksNever { - } - - err = ctrl.runHook(scaffoldFS, cfg.outputfs, scaffold.PostScaffoldScripts, vars, cfg.noPrompt) - if err != nil { - return err + err = ctrl.runHook(scaffoldFS, cfg.outputfs, scaffold.PostScaffoldScripts, vars, cfg.noPrompt) + if err != nil { + return err + } } if !cfg.noPrompt && p.Conf.Messages.Post != "" { From 4d5d8c2b9a1e52a26f317e404dda0e05ce4ebee9 Mon Sep 17 00:00:00 2001 From: Hayden <64056131+hay-kot@users.noreply.github.com> Date: Sun, 2 Jun 2024 12:49:53 -0500 Subject: [PATCH 4/9] linter fixes --- app/core/rwfs/os.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/core/rwfs/os.go b/app/core/rwfs/os.go index aec890f..70e6af6 100644 --- a/app/core/rwfs/os.go +++ b/app/core/rwfs/os.go @@ -75,7 +75,7 @@ func writeHook(name string, data []byte) (string, error) { tmp := f.Name() - err = os.Chmod(tmp, 0700) + err = os.Chmod(tmp, 0o700) if err != nil { return tmp, err } From 270762b1fc7885010ed014cba91592a364c19e86 Mon Sep 17 00:00:00 2001 From: Hayden <64056131+hay-kot@users.noreply.github.com> Date: Sun, 2 Jun 2024 12:49:57 -0500 Subject: [PATCH 5/9] tidy --- go.mod | 1 - go.sum | 17 ----------------- 2 files changed, 18 deletions(-) diff --git a/go.mod b/go.mod index 3ea2d72..6f4d1f0 100644 --- a/go.mod +++ b/go.mod @@ -32,7 +32,6 @@ require ( github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/cloudflare/circl v1.3.7 // indirect - github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect github.com/cyphar/filepath-securejoin v0.2.4 // indirect github.com/davecgh/go-spew v1.1.1 // indirect diff --git a/go.sum b/go.sum index 478e9ad..5920642 100644 --- a/go.sum +++ b/go.sum @@ -30,12 +30,8 @@ github.com/bradleyjkemp/cupaloy/v2 v2.8.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1l github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/catppuccin/go v0.2.0 h1:ktBeIrIP42b/8FGiScP9sgrWOss3lw0Z5SktRoithGA= github.com/catppuccin/go v0.2.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= -github.com/charmbracelet/bubbles v0.17.2-0.20240108170749-ec883029c8e6 h1:6nVCV8pqGaeyxetur3gpX3AAaiyKgzjIoCPV3NXKZBE= -github.com/charmbracelet/bubbles v0.17.2-0.20240108170749-ec883029c8e6/go.mod h1:9HxZWlkCqz2PRwsCbYl7a3KXvGzFaDHpYbSYMJ+nE3o= github.com/charmbracelet/bubbles v0.18.0 h1:PYv1A036luoBGroX6VWjQIE9Syf2Wby2oOl/39KLfy0= github.com/charmbracelet/bubbles v0.18.0/go.mod h1:08qhZhtIwzgrtBjAcJnij1t1H0ZRjwHyGsy6AL11PSw= -github.com/charmbracelet/bubbletea v0.25.0 h1:bAfwk7jRz7FKFl9RzlIULPkStffg5k6pNt5dywy4TcM= -github.com/charmbracelet/bubbletea v0.25.0/go.mod h1:EN3QDR1T5ZdWmdfDzYcqOCAps45+QIJbLOBxmVNWNNg= github.com/charmbracelet/bubbletea v0.26.2 h1:Eeb+n75Om9gQ+I6YpbCXQRKHt5Pn4vMwusQpwLiEgJQ= github.com/charmbracelet/bubbletea v0.26.2/go.mod h1:6I0nZ3YHUrQj7YHIHlM8RySX4ZIthTliMY+W8X8b+Gs= github.com/charmbracelet/glamour v0.7.0 h1:2BtKGZ4iVJCDfMF229EzbeR1QRKLWztO9dMtjmqZSng= @@ -47,8 +43,6 @@ github.com/charmbracelet/lipgloss v0.10.0/go.mod h1:Wig9DSfvANsxqkRsqj6x87irdy12 github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= -github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY= -github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= @@ -77,8 +71,6 @@ github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMj github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys= github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY= -github.com/go-sprout/sprout v0.3.0 h1:8reB5lMgCwNVQB/vDkQPs5C0vp3Jh8/4xP7kGJF6bUg= -github.com/go-sprout/sprout v0.3.0/go.mod h1:BG7Zrds7XG7VZvLAkiT3pOK9rLQ6HSJIB4lAXzLdtaA= github.com/go-sprout/sprout v0.4.0 h1:YmXYSxQvlSgShOqSwFRAklph7pW0B05X2A/JL9NCxhs= github.com/go-sprout/sprout v0.4.0/go.mod h1:HcSyVucdA2jn2UOB3793MmWDtf+nSIAs4HV+CZ7YrT4= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= @@ -156,8 +148,6 @@ github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUc github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= -github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0= -github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= @@ -213,8 +203,6 @@ golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= -golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -228,14 +216,11 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -243,8 +228,6 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= -golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= -golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= From 9f9ce84de159e1303aa8aa63959012d08d83dbd8 Mon Sep 17 00:00:00 2001 From: Hayden <64056131+hay-kot@users.noreply.github.com> Date: Sun, 2 Jun 2024 12:55:03 -0500 Subject: [PATCH 6/9] update hooks docs --- docs/docs/templates/hooks.md | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/docs/docs/templates/hooks.md b/docs/docs/templates/hooks.md index 2f10de3..c658caf 100644 --- a/docs/docs/templates/hooks.md +++ b/docs/docs/templates/hooks.md @@ -3,14 +3,19 @@ # Hooks -Hooks are extensionless files that are stored in the `hooks` subdirectory of your scaffold. They allow you to run scripts at specific points during project generation. They are skipped when the scaffold output directory is an in-memory filesystem or when they are explicitely disabled. The [shebang]() is mandatory and can be set to any interpreter on your system. Template variables are available in the scripts. +Hooks are files that are stored in the `hooks` subdirectory of your scaffold. They allow you to run scripts at specific points during project generation. They are skipped when the scaffold output directory is an in-memory filesystem or when they are explicitely disabled. The [shebang]() is mandatory and can be set to any interpreter on your system. -Currently, only the `post_scaffold` hook is implemented. +**Note That** + +- Template variables are available in the scripts. +- Currently, only the `post_scaffold` hook is implemented. +- Hooks are matched by checking for a string prefix, so `post_scaffold.sh` will execute on the `post_scaffold` hook. +- Only one hook file is allowed per hook. If multiple files are found, only the first is executed. ::: tip Working directory The scripts' working directory is set to the scaffold output directory. ::: -## `post_scaffold` +## `post_scaffold*` -The `post_scaffold` hook is executed after the files have been rendered on the disk, but before the `post` message is printed. It is typically used to fix the formatting of generated files. +The `post_scaffold*` hook is executed after the files have been rendered on the disk, but before the `post` message is printed. It is typically used to fix the formatting of generated files. From 21eaa0db12d938d7898561634c0f96929dfeb5d6 Mon Sep 17 00:00:00 2001 From: Hayden <64056131+hay-kot@users.noreply.github.com> Date: Sun, 2 Jun 2024 13:01:34 -0500 Subject: [PATCH 7/9] cleanup docs --- docs/docs/.vitepress/config.mts | 10 ++-------- docs/docs/templates/hooks.md | 4 ++-- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/docs/docs/.vitepress/config.mts b/docs/docs/.vitepress/config.mts index 4a36dcb..670581c 100644 --- a/docs/docs/.vitepress/config.mts +++ b/docs/docs/.vitepress/config.mts @@ -51,16 +51,10 @@ export default withMermaid( text: "Creating Scaffolds", items: [ { text: "Scaffold File", link: "/templates/scaffold-file" }, + { text: "File Reference", link: "/templates/config-reference" }, { text: "Template Engine", link: "/templates/template-engine" }, - { - text: "File Reference", - link: "/templates/config-reference", - }, { text: "Hooks", link: "/templates/hooks" }, - { - text: "Testing Scaffolds", - link: "/templates/testing-scaffolds", - }, + { text: "Testing Scaffolds", link: "/templates/testing-scaffolds" }, ], }, ], diff --git a/docs/docs/templates/hooks.md b/docs/docs/templates/hooks.md index c658caf..7852ca7 100644 --- a/docs/docs/templates/hooks.md +++ b/docs/docs/templates/hooks.md @@ -16,6 +16,6 @@ Hooks are files that are stored in the `hooks` subdirectory of your scaffold. Th The scripts' working directory is set to the scaffold output directory. ::: -## `post_scaffold*` +## `post_scaffold` -The `post_scaffold*` hook is executed after the files have been rendered on the disk, but before the `post` message is printed. It is typically used to fix the formatting of generated files. +The `post_scaffold` hook is executed after the files have been rendered on the disk, but before the `post` message is printed. It is typically used to fix the formatting of generated files. From 46a8dabb8903097a044d9a7bb3afbb183f8f0baa Mon Sep 17 00:00:00 2001 From: Hayden <64056131+hay-kot@users.noreply.github.com> Date: Sun, 2 Jun 2024 13:09:19 -0500 Subject: [PATCH 8/9] add env override support for settings flags --- main.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/main.go b/main.go index a4ee0e4..676215b 100644 --- a/main.go +++ b/main.go @@ -96,11 +96,12 @@ func main() { Name: "theme", Usage: "theme to use for the scaffold output", Value: "scaffold", - EnvVars: []string{"SCAFFOLD_THEME"}, + EnvVars: []string{"SCAFFOLD_THEME", "SCAFFOLD_SETTINGS_THEME"}, }, &cli.StringFlag{ - Name: "run-hooks", - Usage: "run hooks (never, always, prompt) when provided overrides scaffold rc", + Name: "run-hooks", + Usage: "run hooks (never, always, prompt) when provided overrides scaffold rc", + EnvVars: []string{"SCAFFOLD_SETTINGS_RUN_HOOKS"}, }, }, Before: func(ctx *cli.Context) error { From 0da634ad89ea740d27c5384303813b298a4d63a2 Mon Sep 17 00:00:00 2001 From: Hayden <64056131+hay-kot@users.noreply.github.com> Date: Sun, 2 Jun 2024 13:18:09 -0500 Subject: [PATCH 9/9] tests for unmarshal text --- app/scaffold/rc_test.go | 58 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/app/scaffold/rc_test.go b/app/scaffold/rc_test.go index a9c862e..c3dc23a 100644 --- a/app/scaffold/rc_test.go +++ b/app/scaffold/rc_test.go @@ -3,7 +3,9 @@ package scaffold import ( "bytes" "errors" + "fmt" "io" + "strings" "testing" githttp "github.com/go-git/go-git/v5/plumbing/transport/http" @@ -147,3 +149,59 @@ auth: require.False(t, ok) assert.Nil(t, auth) } + +func Test_RunHooksOption_UnmarshalText(t *testing.T) { + type tcase struct { + namefmt string + inputs []string + want RunHooksOption + wantValid bool + } + + tests := []tcase{ + { + namefmt: "valid for 'never' (%s)", + inputs: []string{"never", "no", "false"}, + want: RunHooksNever, + wantValid: true, + }, + { + namefmt: "valid for 'always' (%s)", + inputs: []string{"always", "yes", "true"}, + want: RunHooksAlways, + wantValid: true, + }, + { + namefmt: "valid for 'prompt' (%s)", + inputs: []string{"prompt", ""}, + want: RunHooksPrompt, + wantValid: true, + }, + { + namefmt: "invalid for any option (%s)", + inputs: []string{"invalid", " "}, + wantValid: false, + }, + } + + for _, tt := range tests { + name := fmt.Sprintf(tt.namefmt, strings.Join(tt.inputs, ", ")) + t.Run(name, func(t *testing.T) { + for _, input := range tt.inputs { + var got RunHooksOption + err := got.UnmarshalText([]byte(input)) + + require.NoError(t, err) // UnmarshalText should _never_ error + + switch { + case tt.wantValid: + assert.Equal(t, tt.want, got) + assert.True(t, got.IsValid()) + default: + assert.NotEqual(t, tt.want, got) + assert.False(t, got.IsValid()) + } + } + }) + } +}