Repository: gruntwork-io/terragrunt (language: go)

Issue:
Terragrunt scaffold
**Describe the solution you'd like**
Implement scaffolding functionality in Terragrunt - pass TF module URL, generate Terragrunt files to use module

**Describe alternatives you've considered**
N/A

**Additional context**
N/A



Discussion / hints:

Fix patch:
diff --git a/cli/app.go b/cli/app.go
index 105eb5557f..4d63627ba0 100644
--- a/cli/app.go
+++ b/cli/app.go
@@ -8,6 +8,8 @@ import (
 	"sort"
 	"strings"
 
+	"github.com/gruntwork-io/terragrunt/cli/commands/scaffold"
+
 	"github.com/gruntwork-io/terragrunt/shell"
 
 	"github.com/gruntwork-io/go-commons/errors"
@@ -75,6 +77,7 @@ func terragruntCommands(opts *options.TerragruntOptions) cli.Commands {
 		renderjson.NewCommand(opts),         // render-json
 		awsproviderpatch.NewCommand(opts),   // aws-provider-patch
 		outputmodulegroups.NewCommand(opts), // output-module-groups
+		scaffold.NewCommand(opts),           // scaffold
 	}
 
 	sort.Sort(cmds)
diff --git a/cli/commands/hclfmt/action.go b/cli/commands/hclfmt/action.go
index 9bcd8f94c3..9b905290c5 100644
--- a/cli/commands/hclfmt/action.go
+++ b/cli/commands/hclfmt/action.go
@@ -46,11 +46,18 @@ func Run(opts *options.TerragruntOptions) error {
 
 	filteredTgHclFiles := []string{}
 	for _, fname := range tgHclFiles {
-		// Ignore any files that are in the .terragrunt-cache
-		if !util.ListContainsElement(strings.Split(fname, "/"), util.TerragruntCacheDir) {
-			filteredTgHclFiles = append(filteredTgHclFiles, fname)
+		skipFile := false
+		// Ignore any files that are in the cache or scaffold dir
+		if util.ListContainsElement(strings.Split(fname, "/"), util.TerragruntCacheDir) {
+			skipFile = true
+		}
+		if util.ListContainsElement(strings.Split(fname, "/"), util.DefaultBoilerplateDir) {
+			skipFile = true
+		}
+		if skipFile {
+			opts.Logger.Debugf("%s was ignored", fname)
 		} else {
-			opts.Logger.Debugf("%s was ignored due to being in the terragrunt cache", fname)
+			filteredTgHclFiles = append(filteredTgHclFiles, fname)
 		}
 	}
 
diff --git a/cli/commands/scaffold/action.go b/cli/commands/scaffold/action.go
new file mode 100644
index 0000000000..a743b5a1ac
--- /dev/null
+++ b/cli/commands/scaffold/action.go
@@ -0,0 +1,413 @@
+package scaffold
+
+import (
+	"fmt"
+	"net/url"
+	"os"
+	"regexp"
+	"strings"
+
+	"github.com/gruntwork-io/terragrunt/pkg/cli"
+
+	"github.com/gruntwork-io/terragrunt/config"
+	"github.com/gruntwork-io/terragrunt/shell"
+
+	"github.com/gruntwork-io/terragrunt/terraform"
+
+	"github.com/gruntwork-io/terragrunt/cli/commands/hclfmt"
+	"github.com/gruntwork-io/terragrunt/util"
+
+	boilerplate_options "github.com/gruntwork-io/boilerplate/options"
+	"github.com/gruntwork-io/boilerplate/templates"
+	"github.com/gruntwork-io/boilerplate/variables"
+	"github.com/gruntwork-io/go-commons/errors"
+	"github.com/gruntwork-io/terragrunt/options"
+	"github.com/gruntwork-io/terratest/modules/files"
+	"github.com/hashicorp/go-getter"
+)
+
+const (
+	sourceUrlTypeHttps = "git-https"
+	sourceUrlTypeGit   = "git-ssh"
+	sourceGitSshUser   = "git"
+
+	sourceUrlTypeVar    = "SourceUrlType"
+	sourceGitSshUserVar = "SourceGitSshUser"
+	refVar              = "Ref"
+	// refParam - ?ref param from url
+	refParam = "ref"
+
+	moduleUrlPattern = `git::([^:]+)://([^/]+)(/.*)`
+	moduleUrlParts   = 4
+
+	defaultBoilerplateConfig = `
+variables:
+  - name: EnableRootInclude
+    description: Should include root module
+    type: bool
+    default: true
+`
+	defaultTerragruntTemplate = `
+# This is a Terragrunt module generated by boilerplate.
+terraform {
+  source = "{{ .sourceUrl }}"
+}
+
+{{if .EnableRootInclude}}
+include "root" {
+  path = find_in_parent_folders()
+}
+{{end}}
+
+
+inputs = {
+  # --------------------------------------------------------------------------------------------------------------------
+  # Required input variables
+  # --------------------------------------------------------------------------------------------------------------------
+  {{ range .requiredVariables }}
+  # Description: {{ .Description }}
+  # Type: {{ .Type }}
+  {{ .Name }} = {{ .DefaultValuePlaceholder }}  # TODO: fill in value
+  {{ end }}
+
+  # --------------------------------------------------------------------------------------------------------------------
+  # Optional input variables
+  # Uncomment the ones you wish to set
+  # --------------------------------------------------------------------------------------------------------------------
+  {{ range .optionalVariables }}
+  # Description: {{ .Description }}
+  # Type: {{ .Type }}
+  # {{ .Name }} = {{ .DefaultValue }}
+  {{ end }}
+}
+`
+)
+
+var moduleUrlRegex = regexp.MustCompile(moduleUrlPattern)
+
+func Run(ctx *cli.Context, opts *options.TerragruntOptions) error {
+	// download remote repo to local
+	var moduleUrl = ""
+	var templateUrl = ""
+	var dirsToClean []string
+	// clean all temp dirs
+	defer func() {
+		for _, dir := range dirsToClean {
+			if err := os.RemoveAll(dir); err != nil {
+				opts.Logger.Warnf("Failed to clean up dir %s: %v", dir, err)
+			}
+		}
+	}()
+
+	// scaffold only in empty directories
+	if empty, err := util.IsDirectoryEmpty(opts.WorkingDir); !empty || err != nil {
+		if err != nil {
+			return err
+		}
+		opts.Logger.Warnf("The working directory %s is not empty.", opts.WorkingDir)
+	}
+
+	if val := ctx.Args().Get(0); val != "" {
+		moduleUrl = val
+	}
+
+	if val := ctx.Args().Get(1); val != "" {
+		templateUrl = val
+	}
+
+	if moduleUrl == "" {
+		return errors.WithStackTrace(NoModuleUrlPassed{})
+	}
+
+	// create temporary directory where to download module
+	tempDir, err := os.MkdirTemp("", "scaffold")
+	if err != nil {
+		return errors.WithStackTrace(err)
+	}
+	dirsToClean = append(dirsToClean, tempDir)
+
+	// prepare variables
+	vars, err := variables.ParseVars(opts.ScaffoldVars, opts.ScaffoldVarFiles)
+	if err != nil {
+		return errors.WithStackTrace(err)
+	}
+
+	// parse module url
+	moduleUrl, err = parseModuleUrl(opts, vars, moduleUrl)
+	if err != nil {
+		return errors.WithStackTrace(err)
+	}
+
+	opts.Logger.Infof("Scaffolding a new Terragrunt module %s to %s", moduleUrl, opts.WorkingDir)
+	if err := getter.GetAny(tempDir, moduleUrl); err != nil {
+		return errors.WithStackTrace(err)
+	}
+
+	// extract variables from downloaded module
+	requiredVariables, optionalVariables, err := parseVariables(opts, tempDir)
+	if err != nil {
+		return errors.WithStackTrace(err)
+	}
+	opts.Logger.Debugf("Parsed %d required variables and %d optional variables", len(requiredVariables), len(optionalVariables))
+
+	// prepare boilerplate files to render Terragrunt files
+	boilerplateDir, err := prepareBoilerplateFiles(opts, templateUrl, tempDir)
+	if err != nil {
+		return errors.WithStackTrace(err)
+	}
+
+	// add additional variables
+	vars["requiredVariables"] = requiredVariables
+	vars["optionalVariables"] = optionalVariables
+
+	vars["sourceUrl"] = moduleUrl
+
+	opts.Logger.Infof("Running boilerplate generation to %s", opts.WorkingDir)
+	boilerplateOpts := &boilerplate_options.BoilerplateOptions{
+		OutputFolder:    opts.WorkingDir,
+		OnMissingKey:    boilerplate_options.DefaultMissingKeyAction,
+		OnMissingConfig: boilerplate_options.DefaultMissingConfigAction,
+		Vars:            vars,
+		DisableShell:    true,
+		DisableHooks:    true,
+		NonInteractive:  opts.NonInteractive,
+		TemplateFolder:  boilerplateDir,
+	}
+
+	emptyDep := variables.Dependency{}
+	if err := templates.ProcessTemplate(boilerplateOpts, boilerplateOpts, emptyDep); err != nil {
+		return errors.WithStackTrace(err)
+	}
+
+	opts.Logger.Infof("Running fmt on generated code %s", opts.WorkingDir)
+	if err := hclfmt.Run(opts); err != nil {
+		return errors.WithStackTrace(err)
+	}
+
+	opts.Logger.Info("Scaffolding completed")
+
+	return nil
+}
+
+// prepareBoilerplateFiles prepares boilerplate files.
+func prepareBoilerplateFiles(opts *options.TerragruntOptions, templateUrl string, tempDir string) (string, error) {
+	// identify template url
+	templateDir := ""
+	if templateUrl != "" {
+		// process template url if was passed
+		parsedTemplateUrl, err := terraform.ToSourceUrl(templateUrl, tempDir)
+		if err != nil {
+			return "", errors.WithStackTrace(err)
+		}
+		parsedTemplateUrl, err = rewriteTemplateUrl(opts, parsedTemplateUrl)
+		if err != nil {
+			return "", errors.WithStackTrace(err)
+		}
+		// regenerate template url with all changes
+		templateUrl = parsedTemplateUrl.String()
+
+		// prepare temporary directory for template
+		templateDir, err = os.MkdirTemp("", "template")
+		if err != nil {
+			return "", errors.WithStackTrace(err)
+		}
+
+		// downloading template
+		opts.Logger.Infof("Using template from %s", templateUrl)
+		if err := getter.GetAny(templateDir, templateUrl); err != nil {
+			return "", errors.WithStackTrace(err)
+		}
+	}
+	// prepare boilerplate dir
+	boilerplateDir := util.JoinPath(tempDir, util.DefaultBoilerplateDir)
+	// use template dir as boilerplate dir
+	if templateDir != "" {
+		boilerplateDir = templateDir
+	}
+
+	// if boilerplate dir is not found, create one with default template
+	if !files.IsExistingDir(boilerplateDir) {
+		// no default boilerplate dir, create one
+		defaultTempDir, err := os.MkdirTemp("", "boilerplate")
+		if err != nil {
+			return "", errors.WithStackTrace(err)
+		}
+		boilerplateDir = defaultTempDir
+		if err := os.WriteFile(util.JoinPath(boilerplateDir, "terragrunt.hcl"), []byte(defaultTerragruntTemplate), 0644); err != nil {
+			return "", errors.WithStackTrace(err)
+		}
+		if err := os.WriteFile(util.JoinPath(boilerplateDir, "boilerplate.yml"), []byte(defaultBoilerplateConfig), 0644); err != nil {
+			return "", errors.WithStackTrace(err)
+		}
+	}
+	return boilerplateDir, nil
+}
+
+// parseVariables - parse variables from tf files.
+func parseVariables(opts *options.TerragruntOptions, moduleDir string) ([]*config.ParsedVariable, []*config.ParsedVariable, error) {
+	inputs, err := config.ParseVariables(opts, moduleDir)
+	if err != nil {
+		return nil, nil, errors.WithStackTrace(err)
+	}
+
+	// separate variables that require value and with default value
+	var requiredVariables []*config.ParsedVariable
+	var optionalVariables []*config.ParsedVariable
+
+	for _, value := range inputs {
+		if value.DefaultValue == "" {
+			requiredVariables = append(requiredVariables, value)
+		} else {
+			optionalVariables = append(optionalVariables, value)
+		}
+	}
+	return requiredVariables, optionalVariables, nil
+}
+
+// parseModuleUrl - parse module url and rewrite it if required
+func parseModuleUrl(opts *options.TerragruntOptions, vars map[string]interface{}, moduleUrl string) (string, error) {
+	parsedModuleUrl, err := terraform.ToSourceUrl(moduleUrl, opts.WorkingDir)
+	if err != nil {
+		return "", errors.WithStackTrace(err)
+	}
+	moduleUrl = parsedModuleUrl.String()
+
+	// rewrite module url, if required
+	parsedModuleUrl, err = rewriteModuleUrl(opts, vars, moduleUrl)
+	if err != nil {
+		return "", errors.WithStackTrace(err)
+	}
+
+	// add ref to module url, if required
+	parsedModuleUrl, err = addRefToModuleUrl(opts, parsedModuleUrl, vars)
+	if err != nil {
+		return "", errors.WithStackTrace(err)
+	}
+
+	// regenerate module url with all changes
+	return parsedModuleUrl.String(), nil
+}
+
+// rewriteModuleUrl rewrites module url to git ssh if required
+// github.com/gruntwork-io/terragrunt.git//test/fixture-inputs => git::https://github.com/gruntwork-io/terragrunt.git//test/fixture-inputs
+func rewriteModuleUrl(opts *options.TerragruntOptions, vars map[string]interface{}, moduleUrl string) (*url.URL, error) {
+	var updatedModuleUrl = moduleUrl
+	sourceUrlType := sourceUrlTypeHttps
+	if value, found := vars[sourceUrlTypeVar]; found {
+		sourceUrlType = fmt.Sprintf("%s", value)
+	}
+
+	// expand module url
+	parsedValue, err := parseUrl(opts, moduleUrl)
+	if err != nil {
+		opts.Logger.Warnf("Failed to parse module url %s", moduleUrl)
+		parsedModuleUrl, err := terraform.ToSourceUrl(updatedModuleUrl, opts.WorkingDir)
+		if err != nil {
+			return nil, errors.WithStackTrace(err)
+		}
+		return parsedModuleUrl, nil
+	}
+	// try to rewrite module url if is https and is requested to be git
+	// git::https://github.com/gruntwork-io/terragrunt.git//test/fixture-inputs => git::ssh://git@github.com/gruntwork-io/terragrunt.git//test/fixture-inputs
+	if parsedValue.scheme == "https" && sourceUrlType == sourceUrlTypeGit {
+		gitUser := sourceGitSshUser
+		if value, found := vars[sourceGitSshUserVar]; found {
+			gitUser = fmt.Sprintf("%s", value)
+		}
+		path := strings.TrimPrefix(parsedValue.path, "/")
+		updatedModuleUrl = fmt.Sprintf("%s@%s:%s", gitUser, parsedValue.host, path)
+	}
+
+	// persist changes in url.URL
+	parsedModuleUrl, err := terraform.ToSourceUrl(updatedModuleUrl, opts.WorkingDir)
+	if err != nil {
+		return nil, errors.WithStackTrace(err)
+	}
+	return parsedModuleUrl, nil
+}
+
+// rewriteTemplateUrl rewrites template url with reference to tag
+// github.com/denis256/terragrunt-tests.git//scaffold/base-template => github.com/denis256/terragrunt-tests.git//scaffold/base-template?ref=v0.53.8
+func rewriteTemplateUrl(opts *options.TerragruntOptions, parsedTemplateUrl *url.URL) (*url.URL, error) {
+	var updatedTemplateUrl = parsedTemplateUrl
+	var templateParams = updatedTemplateUrl.Query()
+	ref := templateParams.Get(refParam)
+	if ref == "" {
+		rootSourceUrl, _, err := terraform.SplitSourceUrl(updatedTemplateUrl, opts.Logger)
+		if err != nil {
+			return nil, errors.WithStackTrace(err)
+		}
+		tag, err := shell.GitLastReleaseTag(opts, rootSourceUrl)
+		if err != nil || tag == "" {
+			opts.Logger.Warnf("Failed to find last release tag for URL %s, so will not add a ref param to the URL", rootSourceUrl)
+		} else {
+			templateParams.Add(refParam, tag)
+			updatedTemplateUrl.RawQuery = templateParams.Encode()
+		}
+	}
+	return updatedTemplateUrl, nil
+}
+
+// addRefToModuleUrl adds ref to module url if is passed through variables or find it from git tags
+func addRefToModuleUrl(opts *options.TerragruntOptions, parsedModuleUrl *url.URL, vars map[string]interface{}) (*url.URL, error) {
+	var moduleUrl = parsedModuleUrl
+	// append ref to source url, if is passed through variables or find it from git tags
+	params := moduleUrl.Query()
+	refReplacement, refVarPassed := vars[refVar]
+	if refVarPassed {
+		params.Set(refParam, fmt.Sprintf("%s", refReplacement))
+		moduleUrl.RawQuery = params.Encode()
+	}
+	ref := params.Get(refParam)
+	if ref == "" {
+		// if ref is not passed, find last release tag
+		// git::https://github.com/gruntwork-io/terragrunt.git//test/fixture-inputs => git::https://github.com/gruntwork-io/terragrunt.git//test/fixture-inputs?ref=v0.53.8
+		rootSourceUrl, _, err := terraform.SplitSourceUrl(moduleUrl, opts.Logger)
+		if err != nil {
+			return nil, errors.WithStackTrace(err)
+		}
+
+		tag, err := shell.GitLastReleaseTag(opts, rootSourceUrl)
+		if err != nil || tag == "" {
+			opts.Logger.Warnf("Failed to find last release tag for %s", rootSourceUrl)
+		} else {
+			params.Add(refParam, tag)
+			moduleUrl.RawQuery = params.Encode()
+		}
+	}
+	return moduleUrl, nil
+}
+
+// parseUrl parses module url to scheme, host and path
+func parseUrl(opts *options.TerragruntOptions, moduleUrl string) (*parsedUrl, error) {
+	matches := moduleUrlRegex.FindStringSubmatch(moduleUrl)
+	if len(matches) != moduleUrlParts {
+		opts.Logger.Warnf("Failed to parse module url %s", moduleUrl)
+		return nil, failedToParseUrlError{}
+	}
+	return &parsedUrl{
+		scheme: matches[1],
+		host:   matches[2],
+		path:   matches[3],
+	}, nil
+}
+
+type parsedUrl struct {
+	scheme string
+	host   string
+	path   string
+}
+
+type failedToParseUrlError struct {
+}
+
+func (err failedToParseUrlError) Error() string {
+	return "Failed to parse Url."
+}
+
+type NoModuleUrlPassed struct {
+}
+
+func (err NoModuleUrlPassed) Error() string {
+	return "No module URL passed."
+}
diff --git a/cli/commands/scaffold/command.go b/cli/commands/scaffold/command.go
new file mode 100644
index 0000000000..fd797ac232
--- /dev/null
+++ b/cli/commands/scaffold/command.go
@@ -0,0 +1,37 @@
+package scaffold
+
+import (
+	"github.com/gruntwork-io/terragrunt/options"
+	"github.com/gruntwork-io/terragrunt/pkg/cli"
+)
+
+const (
+	CommandName = "scaffold"
+	Var         = "var"
+	VarFile     = "var-file"
+)
+
+func NewFlags(opts *options.TerragruntOptions) cli.Flags {
+	return cli.Flags{
+		&cli.SliceFlag[string]{
+			Name:        Var,
+			Destination: &opts.ScaffoldVars,
+			Usage:       "Variables for usage in scaffolding.",
+		},
+		&cli.SliceFlag[string]{
+			Name:        VarFile,
+			Destination: &opts.ScaffoldVarFiles,
+			Usage:       "Files with variables to be used in modules scaffolding.",
+		},
+	}
+}
+
+func NewCommand(opts *options.TerragruntOptions) *cli.Command {
+	return &cli.Command{
+		Name:                   CommandName,
+		Usage:                  "Scaffold a new Terragrunt module.",
+		DisallowUndefinedFlags: true,
+		Flags:                  NewFlags(opts).Sort(),
+		Action:                 func(ctx *cli.Context) error { return Run(ctx, opts.OptionsFromContext(ctx)) },
+	}
+}
diff --git a/config/variable.go b/config/variable.go
new file mode 100644
index 0000000000..cbc5236421
--- /dev/null
+++ b/config/variable.go
@@ -0,0 +1,174 @@
+package config
+
+import (
+	"encoding/json"
+	"fmt"
+	"os"
+
+	"github.com/gruntwork-io/go-commons/errors"
+	"github.com/gruntwork-io/terragrunt/options"
+	"github.com/gruntwork-io/terragrunt/util"
+	"github.com/hashicorp/hcl/v2"
+	"github.com/hashicorp/hcl/v2/hclparse"
+	"github.com/hashicorp/hcl/v2/hclsyntax"
+	"github.com/zclconf/go-cty/cty"
+	ctyjson "github.com/zclconf/go-cty/cty/json"
+)
+
+// ParsedVariable structure with input name, default value and description.
+type ParsedVariable struct {
+	Name                    string
+	Description             string
+	Type                    string
+	DefaultValue            string
+	DefaultValuePlaceholder string
+}
+
+// ParseVariables - parse variables from tf files.
+func ParseVariables(opts *options.TerragruntOptions, directoryPath string) ([]*ParsedVariable, error) {
+	// list all tf files
+	tfFiles, err := util.ListTfFiles(directoryPath)
+	if err != nil {
+		return nil, errors.WithStackTrace(err)
+	}
+	parser := hclparse.NewParser()
+
+	// iterate over files and parse variables.
+	var parsedInputs []*ParsedVariable
+	for _, tfFile := range tfFiles {
+		content, err := os.ReadFile(tfFile)
+		if err != nil {
+			opts.Logger.Warnf("Error reading file %s: %v", tfFile, err)
+			return nil, errors.WithStackTrace(err)
+		}
+		file, diags := parser.ParseHCL(content, tfFile)
+		if diags.HasErrors() {
+			opts.Logger.Warnf("Failed to parse HCL in file %s: %v", tfFile, diags)
+			return nil, errors.WithStackTrace(err)
+		}
+
+		ctx := &hcl.EvalContext{}
+
+		if body, ok := file.Body.(*hclsyntax.Body); ok {
+			for _, block := range body.Blocks {
+				if block.Type == "variable" {
+					if len(block.Labels[0]) > 0 {
+						// extract variable attributes
+						name := block.Labels[0]
+						descriptionAttr, err := readBlockAttribute(ctx, block, "description")
+						descriptionAttrText := ""
+						if err != nil {
+							opts.Logger.Warnf("Failed to read descriptionAttr for %s %v", name, err)
+							descriptionAttr = nil
+						}
+						if descriptionAttr != nil {
+							descriptionAttrText = descriptionAttr.AsString()
+						} else {
+							descriptionAttrText = fmt.Sprintf("(variable %s did not define a description)", name)
+						}
+
+						typeAttr, err := readBlockAttribute(ctx, block, "type")
+						typeAttrText := ""
+						if err != nil {
+							opts.Logger.Warnf("Failed to read type attribute for %s %v", name, err)
+							descriptionAttr = nil
+						}
+						if typeAttr != nil {
+							typeAttrText = typeAttr.AsString()
+						} else {
+							typeAttrText = fmt.Sprintf("(variable %s does not define a type)", name)
+						}
+
+						defaultValue, err := readBlockAttribute(ctx, block, "default")
+						if err != nil {
+							opts.Logger.Warnf("Failed to read default value for %s %v", name, err)
+							defaultValue = nil
+						}
+
+						defaultValueText := ""
+						if defaultValue != nil {
+							jsonBytes, err := ctyjson.Marshal(*defaultValue, cty.DynamicPseudoType)
+							if err != nil {
+								return nil, errors.WithStackTrace(err)
+							}
+
+							var ctyJsonOutput ctyJsonValue
+							if err := json.Unmarshal(jsonBytes, &ctyJsonOutput); err != nil {
+								return nil, errors.WithStackTrace(err)
+							}
+
+							jsonBytes, err = json.Marshal(ctyJsonOutput.Value)
+							if err != nil {
+								return nil, errors.WithStackTrace(err)
+							}
+							defaultValueText = string(jsonBytes)
+						}
+
+						input := &ParsedVariable{
+							Name:                    name,
+							Type:                    typeAttrText,
+							Description:             descriptionAttrText,
+							DefaultValue:            defaultValueText,
+							DefaultValuePlaceholder: generateDefaultValue(typeAttrText),
+						}
+
+						parsedInputs = append(parsedInputs, input)
+					}
+				}
+			}
+		}
+	}
+	return parsedInputs, nil
+}
+
+// generateDefaultValue - generate hcl default value
+// HCL type of variable https://developer.hashicorp.com/packer/docs/templates/hcl_templates/variables#type-constraints
+func generateDefaultValue(variableType string) string {
+	switch variableType {
+	case "number":
+		return "0"
+	case "bool":
+		return "false"
+	case "list":
+		return "[]"
+	case "map":
+		return "{}"
+	case "object":
+		return "{}"
+	}
+	// fallback to empty value
+	return "\"\""
+}
+
+type ctyJsonValue struct {
+	Value interface{}
+	Type  interface{}
+}
+
+// readBlockAttribute - hcl block attribute.
+func readBlockAttribute(ctx *hcl.EvalContext, block *hclsyntax.Block, name string) (*cty.Value, error) {
+	if attr, ok := block.Body.Attributes[name]; ok {
+		if attr.Expr != nil {
+			if call, ok := attr.Expr.(*hclsyntax.FunctionCallExpr); ok {
+				result := cty.StringVal(call.Name)
+				return &result, nil
+			}
+			// check if first var is traversal
+			if len(attr.Expr.Variables()) > 0 {
+				v := attr.Expr.Variables()[0]
+				// check if variable is traversal
+				if varTr, ok := v[0].(hcl.TraverseRoot); ok {
+					result := cty.StringVal(varTr.Name)
+					return &result, nil
+				}
+			}
+
+			value, err := attr.Expr.Value(ctx)
+			if err != nil {
+				return nil, err
+			}
+			return &value, nil
+		}
+	}
+	return nil, nil
+}
diff --git a/docs/_docs/02_features/scaffold.md b/docs/_docs/02_features/scaffold.md
new file mode 100644
index 0000000000..2fb3032a18
--- /dev/null
+++ b/docs/_docs/02_features/scaffold.md
@@ -0,0 +1,122 @@
+---
+layout: scaffold
+title: Scaffold
+category: features
+categories_url: features
+excerpt: Learn how to scaffold Terragrunt projects.
+tags: ["scaffold"]
+order: 205
+nav_title: Documentation
+nav_title_link: /docs/
+---
+
+## Scaffold
+
+Terragrunt scaffolding can generate files for you automatically using [boilerplate](https://github.com/gruntwork-io/boilerplate) templates.
+
+Currently, one boilerplate template is supported out-of-the-box, which you can use to generate a best-practices `terragrunt.hcl` that configures a Terraform module for deployment:
+```
+terragrunt scaffold <MODULE_URL> [TEMPLATE_URL] [--var] [--var-file]
+```
+Description:
+* `MODULE_URL` - URL to a Terraform module. Can be a local file path, git URL, registry URL, or any other [module source URL](https://developer.hashicorp.com/terraform/language/modules/sources).
+* `TEMPLATE_URL` - Optional URL to a custom boilerplate template to use to generate HCL files. Can be a local file path, git URL, registry URL, or any other [module source URL](https://developer.hashicorp.com/terraform/language/modules/sources). If not specified, Terragrunt will:
+    - Look for a `.boilerplate` folder in the module at `MODULE_URL`, and if found, use the the boilerplate template in that folder.
+    - Failing to find that, Terragrunt will use a boilerplate template that is built-in, which creates a best-practices `terragrunt.hcl` for deploying a single Terraform module.
+
+
+For example, here's how you can generate a `terragrunt.hcl` file to configure an [example MySQL Terraform module](https://github.com/gruntwork-io/terragrunt-infrastructure-modules-example/tree/master/mysql) for deployment:
+
+```bash
+terragrunt scaffold github.com/gruntwork-io/terragrunt-infrastructure-modules-example//mysql
+```
+
+This will create a `terragrunt.hcl` in your current working directory, with roughly the following contents:
+
+```hcl
+
+# This is a Terragrunt module generated by boilerplate.
+terraform {
+  source = "git::https://github.com/gruntwork-io/terragrunt-infrastructure-modules-example.git//mysql?ref=v0.7.0"
+}
+
+inputs = {
+  # --------------------------------------------------------------------------------------------------------------------
+  # Required input variables
+  # --------------------------------------------------------------------------------------------------------------------
+  
+  # Type: string
+  # Description: The AWS region to deploy to (e.g. us-east-1)
+  aws_region = "" # TODO: fill in value
+
+  # Type: string
+  # Description: The name of the DB
+  name = "" # TODO: fill in value
+
+  # Type: string
+  # Description: The instance class of the DB (e.g. db.t2.micro)
+  instance_class = "" # TODO: fill in value
+
+  # (... full list of inputs omitted for brevity ...)
+}
+```
+Important notes:
+* The `source` URL is configured for you automatically, with the `ref` pointing to the latest "release" tag of the module (found by scanning git tags).
+* The `inputs` section is generated for you automatically, and will list all required and optional variables from the module, with their types, descriptions, and defaults, so you can easily fill them in to configure the module as you like.
+
+#### Custom templates for scaffolding
+
+Terragrunt has a basic template built-in for rendering `terragrunt.hcl` files, but you can provide your own templates to customize what code is generated! Scaffolding is done via [boilerplate](https://github.com/gruntwork-io/boilerplate), and Terragrunt allows you to specify custom boilerplate templates via two mechanisms:
+1. You can specify a custom boilerplate template to use as the second argument of the `scaffold` command.
+1. You can define a custom boilerplate template in a `.boilerplate` subfolder of your module.
+
+If you define input variables in your boilerplate template, Terragrunt will prompt users for the values. Those values can also be passed in via `--var` and `--var-file` arguments.
+There are also a set of variables that Terragrunt will automatically expose to your boilerplate templates for rendering:
+* `sourceUrl`         - URL to module
+* `requiredVariables` - list of required variables in the module being scaffolded (see below)
+* `optionalVariables` - list of optional variables in the module being scaffolded (see below)
+
+The elements in the `requiredVariables` and `optionalVariables` lists are structs with the following fields:
+* `Name`  - variable name
+* `Description` - variable description
+* `Type` - variable type (string, number, bool, list, map, object) [Type Constants](https://developer.hashicorp.com/packer/docs/templates/hcl_templates/variables#type-constraints)
+* `DefaultValue`  - variable default value
+* `DefaultValuePlaceholder` - default value placeholder, string = "", number = 0 etc.
+
+Optional variables which can be passed to `scaffold` command:
+* `Ref` - git tag or branch name for module to be used
+* `EnableRootInclude` - add in default `terragrunt.hcl` inclusion for the root module, by default `true`
+* `SourceUrlType` - if set to `git-ssh` module url will be converted to Git/SSH format
+* `SourceGitSshUser` - git user for Git/SSH format, by default `git`
+
+#### Examples
+
+Scaffold new project but use specific module version:
+```
+terragrunt scaffold github.com/gruntwork-io/terragrunt.git//test/fixture-inputs --var=Ref=v0.53.1
+```
+Scaffold new project but use Git/SSH URLs:
+```
+terragrunt scaffold github.com/gruntwork-io/terragrunt.git//test/fixture-inputs --var=SourceUrlType=git-ssh
+
+# terragrunt.hcl
+terraform {
+  source = "git::ssh://git@github.com/gruntwork-io/terragrunt.git//test/fixture-inputs?ref=v0.53.8"
+}
+```
+Scaffold new project using template inside of git repo:
+```
+terragrunt scaffold github.com/gruntwork-io/terragrunt.git//test/fixture-scaffold/module-with-template
+# will be used template from .boilerplate directory to generate terragrunt.hcl
+```
+
+Scaffold new project using external template:
+```
+terragrunt scaffold github.com/gruntwork-io/terragrunt.git//test/fixture-inputs git@github.com/gruntwork-io/terragrunt.git//test/fixture-scaffold/external-template
+
+# will be created: external-template.txt terragrunt.hcl , files from external template 
+```
+
+
+
+
diff --git a/go.mod b/go.mod
index f60627f8e1..96b94ccf9f 100644
--- a/go.mod
+++ b/go.mod
@@ -5,7 +5,7 @@ go 1.21
 require (
 	cloud.google.com/go/storage v1.30.1
 	github.com/aws/aws-sdk-go v1.46.6
-	github.com/creack/pty v1.1.11
+	github.com/creack/pty v1.1.17
 	github.com/fatih/structs v1.1.0
 	github.com/go-errors/errors v1.4.2
 	github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
@@ -56,8 +56,10 @@ require (
 )
 
 require (
+	github.com/gruntwork-io/boilerplate v0.5.7
 	github.com/gruntwork-io/go-commons v0.17.1
 	github.com/gruntwork-io/gruntwork-cli v0.7.0
+	github.com/posener/complete v1.2.3
 	github.com/urfave/cli/v2 v2.25.5
 	golang.org/x/term v0.13.0
 	golang.org/x/text v0.13.0
@@ -68,6 +70,7 @@ require (
 	cloud.google.com/go/compute/metadata v0.2.3 // indirect
 	cloud.google.com/go/iam v1.1.3 // indirect
 	filippo.io/age v1.0.0 // indirect
+	github.com/AlecAivazis/survey/v2 v2.3.4 // indirect
 	github.com/Azure/azure-sdk-for-go v63.3.0+incompatible // indirect
 	github.com/Azure/go-autorest v14.2.0+incompatible // indirect
 	github.com/Azure/go-autorest/autorest v0.11.26 // indirect
@@ -78,7 +81,9 @@ require (
 	github.com/Azure/go-autorest/autorest/validation v0.3.1 // indirect
 	github.com/Azure/go-autorest/logger v0.2.1 // indirect
 	github.com/Azure/go-autorest/tracing v0.6.0 // indirect
+	github.com/Masterminds/goutils v1.1.1 // indirect
 	github.com/Masterminds/semver/v3 v3.2.1 // indirect
+	github.com/Masterminds/sprig/v3 v3.2.2 // indirect
 	github.com/ProtonMail/go-crypto v0.0.0-20220407094043-a94812496cf5 // indirect
 	github.com/agext/levenshtein v1.2.3 // indirect
 	github.com/apparentlymart/go-cidr v1.1.0 // indirect
@@ -86,6 +91,8 @@ require (
 	github.com/apparentlymart/go-versions v1.0.1 // indirect
 	github.com/armon/go-metrics v0.3.10 // indirect
 	github.com/armon/go-radix v1.0.0 // indirect
+	github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a // indirect
+	github.com/atomicgo/cursor v0.0.1 // indirect
 	github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect
 	github.com/blang/semver v3.5.1+incompatible // indirect
 	github.com/bmatcuk/doublestar v1.1.5 // indirect
@@ -97,16 +104,19 @@ require (
 	github.com/docker/go-units v0.5.0 // indirect
 	github.com/fatih/color v1.15.0 // indirect
 	github.com/frankban/quicktest v1.14.5 // indirect
+	github.com/go-ozzo/ozzo-validation v3.6.0+incompatible // indirect
 	github.com/go-sql-driver/mysql v1.5.0 // indirect
 	github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
 	github.com/golang/protobuf v1.5.3 // indirect
 	github.com/google/go-cmp v0.6.0 // indirect
 	github.com/google/go-github/v35 v35.3.0 // indirect
+	github.com/google/go-jsonnet v0.18.0 // indirect
 	github.com/google/go-querystring v1.1.0 // indirect
 	github.com/google/s2a-go v0.1.7 // indirect
 	github.com/google/uuid v1.4.0 // indirect
 	github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
 	github.com/googleapis/gax-go/v2 v2.12.0 // indirect
+	github.com/gookit/color v1.5.0 // indirect
 	github.com/goware/prefixer v0.0.0-20160118172347-395022866408 // indirect
 	github.com/hashicorp/errwrap v1.1.0 // indirect
 	github.com/hashicorp/go-hclog v1.5.0 // indirect
@@ -124,13 +134,18 @@ require (
 	github.com/hashicorp/terraform-svchost v0.0.1 // indirect
 	github.com/hashicorp/yamux v0.0.0-20211028200310-0bc27b27de87 // indirect
 	github.com/howeyc/gopass v0.0.0-20210920133722-c8aef6fb66ef // indirect
+	github.com/huandu/xstrings v1.3.2 // indirect
+	github.com/inancgumus/screen v0.0.0-20190314163918-06e984b86ed3 // indirect
 	github.com/jessevdk/go-flags v1.5.0 // indirect
 	github.com/jmespath/go-jmespath v0.4.0 // indirect
 	github.com/jstemmer/go-junit-report v1.0.0 // indirect
+	github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
 	github.com/kr/pretty v0.3.1 // indirect
 	github.com/lib/pq v1.10.5 // indirect
 	github.com/mattn/go-colorable v0.1.13 // indirect
 	github.com/mattn/go-isatty v0.0.17 // indirect
+	github.com/mattn/go-runewidth v0.0.13 // indirect
+	github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
 	github.com/mitchellh/copystructure v1.2.0 // indirect
 	github.com/mitchellh/go-testing-interface v1.14.1 // indirect
 	github.com/mitchellh/panicwrap v1.0.0 // indirect
@@ -140,16 +155,20 @@ require (
 	github.com/pierrec/lz4 v2.6.1+incompatible // indirect
 	github.com/pkg/errors v0.9.1 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
-	github.com/posener/complete v1.2.3 // indirect
+	github.com/pterm/pterm v0.12.41 // indirect
+	github.com/rivo/uniseg v0.2.0 // indirect
 	github.com/russross/blackfriday/v2 v2.1.0 // indirect
 	github.com/ryanuber/go-glob v1.0.0 // indirect
+	github.com/shopspring/decimal v1.2.0 // indirect
 	github.com/sourcegraph/go-lsp v0.0.0-20200429204803-219e11d77f5d // indirect
 	github.com/sourcegraph/jsonrpc2 v0.2.0 // indirect
 	github.com/spf13/afero v1.9.5 // indirect
+	github.com/spf13/cast v1.3.1 // indirect
 	github.com/terraform-linters/tflint-plugin-sdk v0.17.0 // indirect
 	github.com/terraform-linters/tflint-ruleset-terraform v0.4.0 // indirect
 	github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect
 	github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
+	github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect
 	github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
 	github.com/zclconf/go-cty-yaml v1.0.3 // indirect
 	go.mozilla.org/gopgagent v0.0.0-20170926210634-4d7ea76ff71a // indirect
@@ -167,7 +186,9 @@ require (
 	gopkg.in/ini.v1 v1.66.4 // indirect
 	gopkg.in/square/go-jose.v2 v2.6.0 // indirect
 	gopkg.in/urfave/cli.v1 v1.20.0 // indirect
+	gopkg.in/yaml.v2 v2.4.0 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
+	sigs.k8s.io/yaml v1.2.0 // indirect
 )
 
 // This is necessary to workaround go modules error with terraform importing vault incorrectly.
diff --git a/go.sum b/go.sum
index 904234dd01..4072312334 100644
--- a/go.sum
+++ b/go.sum
@@ -190,6 +190,8 @@ cloud.google.com/go/workflows v1.7.0/go.mod h1:JhSrZuVZWuiDfKEFxU0/F1PQjmpnpcoIS
 dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
 filippo.io/age v1.0.0 h1:V6q14n0mqYU3qKFkZ6oOaF9oXneOviS3ubXsSVBRSzc=
 filippo.io/age v1.0.0/go.mod h1:PaX+Si/Sd5G8LgfCwldsSba3H1DDQZhIhFGkhbHaBq8=
+github.com/AlecAivazis/survey/v2 v2.3.4 h1:pchTU9rsLUSvWEl2Aq9Pv3k0IE2fkqtGxazskAMd9Ng=
+github.com/AlecAivazis/survey/v2 v2.3.4/go.mod h1:hrV6Y/kQCLhIZXGcriDCUBtB3wnN7156gMXJ3+b23xM=
 github.com/Azure/azure-sdk-for-go v45.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
 github.com/Azure/azure-sdk-for-go v47.1.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
 github.com/Azure/azure-sdk-for-go v51.2.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
@@ -240,14 +242,29 @@ github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym
 github.com/ChrisTrenkamp/goxpath v0.0.0-20170922090931-c385f95c6022/go.mod h1:nuWgzSkT5PnyOd+272uUmV0dnAnAn42Mk7PiQC5VzN4=
 github.com/ChrisTrenkamp/goxpath v0.0.0-20190607011252-c5096ec8773d/go.mod h1:nuWgzSkT5PnyOd+272uUmV0dnAnAn42Mk7PiQC5VzN4=
 github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
+github.com/MarvinJWendt/testza v0.1.0/go.mod h1:7AxNvlfeHP7Z/hDQ5JtE3OKYT3XFUeLCDE2DQninSqs=
+github.com/MarvinJWendt/testza v0.2.1/go.mod h1:God7bhG8n6uQxwdScay+gjm9/LnO4D3kkcZX4hv9Rp8=
+github.com/MarvinJWendt/testza v0.2.8/go.mod h1:nwIcjmr0Zz+Rcwfh3/4UhBp7ePKVhuBExvZqnKYWlII=
+github.com/MarvinJWendt/testza v0.2.10/go.mod h1:pd+VWsoGUiFtq+hRKSU1Bktnn+DMCSrDrXDpX2bG66k=
+github.com/MarvinJWendt/testza v0.2.12/go.mod h1:JOIegYyV7rX+7VZ9r77L/eH6CfJHHzXjB69adAhzZkI=
+github.com/MarvinJWendt/testza v0.3.0/go.mod h1:eFcL4I0idjtIx8P9C6KkAuLgATNKpX4/2oUqKc6bF2c=
+github.com/MarvinJWendt/testza v0.3.5 h1:g9krITRRlIsF1eO9sUKXtiTw670gZIIk6T08Keeo1nM=
+github.com/MarvinJWendt/testza v0.3.5/go.mod h1:ExbTpWmA1z2E9HSskvrNcwApoX4F9bID692s10nuHRY=
 github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
+github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
+github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
 github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
+github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
 github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
 github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
 github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o=
+github.com/Masterminds/sprig/v3 v3.2.2 h1:17jRggJu518dr3QaafizSXOjKYp94wKfABxUmyxvxX8=
+github.com/Masterminds/sprig/v3 v3.2.2/go.mod h1:UoaO7Yp8KlPnJIYWTFkMaqPUYKTfGFPhxNuwnnxkKlk=
 github.com/Microsoft/go-winio v0.5.2 h1:a9IhgEQBCUEk6QCdml9CiJGhAws+YwffDHEMp1VMrpA=
 github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
 github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
+github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s=
+github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w=
 github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw=
 github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk=
 github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
@@ -291,6 +308,10 @@ github.com/armon/go-metrics v0.3.10/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb
 github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
 github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI=
 github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
+github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a h1:idn718Q4B6AGu/h5Sxe66HYVdqdGu2l9Iebqhi/AEoA=
+github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
+github.com/atomicgo/cursor v0.0.1 h1:xdogsqa6YYlLfM+GyClC/Lchf7aiMerFiZQn7soTOoU=
+github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk=
 github.com/aws/aws-sdk-go v1.15.78/go.mod h1:E3/ieXAlvM0XWO57iftYVDLLvQ824smPP3ATZkfNZeM=
 github.com/aws/aws-sdk-go v1.31.9/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0=
 github.com/aws/aws-sdk-go v1.37.0/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
@@ -346,8 +367,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:ma
 github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
 github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
 github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
-github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw=
-github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI=
+github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
 github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -380,6 +401,7 @@ github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLi
 github.com/evanphx/json-patch/v5 v5.5.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4=
 github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
 github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
+github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
 github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
 github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
 github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
@@ -410,6 +432,8 @@ github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+
 github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg=
 github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc=
 github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I=
+github.com/go-ozzo/ozzo-validation v3.6.0+incompatible h1:msy24VGS42fKO9K1vLz82/GeYW1cILu7Nuuj1N3BBkE=
+github.com/go-ozzo/ozzo-validation v3.6.0+incompatible/go.mod h1:gsEKFIVnabGBt6mXmxK0MoFy+cZoTJY6mu5Ll3LVLBU=
 github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
 github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
 github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
@@ -490,6 +514,8 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
 github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/google/go-github/v35 v35.3.0 h1:fU+WBzuukn0VssbayTT+Zo3/ESKX9JYWjbZTLOTEyho=
 github.com/google/go-github/v35 v35.3.0/go.mod h1:yWB7uCcVWaUbUP74Aq3whuMySRMatyRmq5U9FTNlbio=
+github.com/google/go-jsonnet v0.18.0 h1:/6pTy6g+Jh1a1I2UMoAODkqELFiVIdOxbNwv0DDzoOg=
+github.com/google/go-jsonnet v0.18.0/go.mod h1:C3fTzyVJDslXdiTqw/bTFk7vSGyCtH3MGRbDfvEwGd0=
 github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
 github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
 github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
@@ -549,6 +575,9 @@ github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qK
 github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
 github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4=
 github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
+github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ=
+github.com/gookit/color v1.5.0 h1:1Opow3+BWDwqor78DcJkJCIwnkviFi+rrOANki9BUFw=
+github.com/gookit/color v1.5.0/go.mod h1:43aQb+Zerm/BWh2GnrgOQm7ffz7tvQXEKV6BFMl7wAo=
 github.com/gophercloud/gophercloud v0.6.1-0.20191122030953-d8ac278c1c9d/go.mod h1:ozGNgr9KYOVATV5jsgHl/ceCDXGuguqOZAzoQ/2vcNM=
 github.com/gophercloud/gophercloud v0.10.1-0.20200424014253-c3bfe50899e5/go.mod h1:gmC5oQqMDOMO1t1gq5DquX/yAU808e/4mzjjDA76+Ss=
 github.com/gophercloud/utils v0.0.0-20200423144003-7c72efc7435d/go.mod h1:ehWUbLQJPqS0Ep+CxeD559hsm9pthPXadJNKwZkp43w=
@@ -563,6 +592,8 @@ github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmg
 github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
 github.com/grpc-ecosystem/grpc-gateway v1.8.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
 github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
+github.com/gruntwork-io/boilerplate v0.5.7 h1:Is6idcUj+HklxCtho+6K3cJde3tNUnVOqWlEWZr6ZwM=
+github.com/gruntwork-io/boilerplate v0.5.7/go.mod h1:MNsJ5fJSld7HMOHtORL6C8JvKE1ySSq4RjQREU5MwZQ=
 github.com/gruntwork-io/go-commons v0.17.1 h1:2KS9wAqrgeOTWj33DSHzDNJ1FCprptWdLFqej+wB8x0=
 github.com/gruntwork-io/go-commons v0.17.1/go.mod h1:S98JcR7irPD1bcruSvnqupg+WSJEJ6xaM89fpUZVISk=
 github.com/gruntwork-io/gruntwork-cli v0.7.0 h1:YgSAmfCj9c61H+zuvHwKfYUwlMhu5arnQQLM4RH+CYs=
@@ -676,14 +707,20 @@ github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKe
 github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
 github.com/hashicorp/yamux v0.0.0-20211028200310-0bc27b27de87 h1:xixZ2bWeofWV68J+x6AzmKuVM/JWCQwkWm6GW/MUR6I=
 github.com/hashicorp/yamux v0.0.0-20211028200310-0bc27b27de87/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
+github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog=
+github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=
 github.com/howeyc/gopass v0.0.0-20210920133722-c8aef6fb66ef h1:A9HsByNhogrvm9cWb28sjiS3i7tcKCkflWFEkHfuAgM=
 github.com/howeyc/gopass v0.0.0-20210920133722-c8aef6fb66ef/go.mod h1:lADxMC39cJJqL93Duh1xhAs4I2Zs8mKS89XWXFGp9cs=
 github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
+github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
+github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw=
 github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
 github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
 github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
 github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA=
 github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
+github.com/inancgumus/screen v0.0.0-20190314163918-06e984b86ed3 h1:fO9A67/izFYFYky7l1pDP5Dr0BTCRkaQJUG6Jm5ehsk=
+github.com/inancgumus/screen v0.0.0-20190314163918-06e984b86ed3/go.mod h1:Ey4uAp+LvIl+s5jRbOHLcZpUDnkjLBROl15fZLwPlTM=
 github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
 github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc=
 github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
@@ -709,10 +746,16 @@ github.com/jstemmer/go-junit-report v1.0.0/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/X
 github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
 github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
 github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
+github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
+github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
 github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
 github.com/klauspost/compress v1.15.11 h1:Lcadnb3RKGin4FYM/orgq0qde+nc15E5Cbqg4B9Sx9c=
 github.com/klauspost/compress v1.15.11/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM=
+github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
+github.com/klauspost/cpuid/v2 v2.0.10/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
+github.com/klauspost/cpuid/v2 v2.0.12 h1:p9dKCg8i4gmOxtv35DvrYoWqYzQrvEVdjQ762Y0OqZE=
+github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
 github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
 github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
@@ -744,8 +787,10 @@ github.com/masterzen/simplexml v0.0.0-20160608183007-4572e39b1ab9/go.mod h1:kCEb
 github.com/masterzen/simplexml v0.0.0-20190410153822-31eea3082786/go.mod h1:kCEbxUJlNDEBNbdQMkPSp6yaKcRXVI6f4ddk8Riv4bc=
 github.com/masterzen/winrm v0.0.0-20200615185753-c42b5136ff88/go.mod h1:a2HXwefeat3evJHxFXSayvRHpYEPJYtErl4uIzfaUqY=
 github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
+github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
 github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
 github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
+github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
 github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
 github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
 github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
@@ -761,11 +806,15 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/
 github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
 github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
 github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
+github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
+github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
 github.com/mattn/go-shellwords v1.0.4/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=
 github.com/mattn/go-zglob v0.0.1/go.mod h1:9fxibJccNxU2cnpIKLRRFA7zX7qhkJIQWBb449FYHOo=
 github.com/mattn/go-zglob v0.0.3 h1:6Ry4EYsScDyt5di4OI6xw1bYhOqfE5S33Z1OPy+d+To=
 github.com/mattn/go-zglob v0.0.3/go.mod h1:9fxibJccNxU2cnpIKLRRFA7zX7qhkJIQWBb449FYHOo=
 github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
+github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=
+github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
 github.com/miekg/dns v1.0.8/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
 github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
 github.com/mitchellh/cli v1.1.2/go.mod h1:6iaV0fGdElS6dPBx0EApTxHrcWvmJphyh2n8YBLPPZ4=
@@ -866,6 +915,17 @@ github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R
 github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
 github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
 github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
+github.com/pterm/pterm v0.12.27/go.mod h1:PhQ89w4i95rhgE+xedAoqous6K9X+r6aSOI2eFF7DZI=
+github.com/pterm/pterm v0.12.29/go.mod h1:WI3qxgvoQFFGKGjGnJR849gU0TsEOvKn5Q8LlY1U7lg=
+github.com/pterm/pterm v0.12.30/go.mod h1:MOqLIyMOgmTDz9yorcYbcw+HsgoZo3BQfg2wtl3HEFE=
+github.com/pterm/pterm v0.12.31/go.mod h1:32ZAWZVXD7ZfG0s8qqHXePte42kdz8ECtRyEejaWgXU=
+github.com/pterm/pterm v0.12.33/go.mod h1:x+h2uL+n7CP/rel9+bImHD5lF3nM9vJj80k9ybiiTTE=
+github.com/pterm/pterm v0.12.36/go.mod h1:NjiL09hFhT/vWjQHSj1athJpx6H8cjpHXNAK5bUw8T8=
+github.com/pterm/pterm v0.12.40/go.mod h1:ffwPLwlbXxP+rxT0GsgDTzS3y3rmpAO1NMjUkGTYf8s=
+github.com/pterm/pterm v0.12.41 h1:e2BRfFo1H9nL8GY0S3ImbZqfZ/YimOk9XtkhoobKJVs=
+github.com/pterm/pterm v0.12.41/go.mod h1:LW/G4J2A42XlTaPTAGRPvbBfF4UXvHWhC6SN7ueU4jU=
+github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
+github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
 github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
 github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
@@ -880,8 +940,11 @@ github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIH
 github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
 github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
 github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
+github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
 github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
 github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
+github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
+github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
 github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
 github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
 github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
@@ -898,6 +961,8 @@ github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasO
 github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
 github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM=
 github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ=
+github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
+github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
 github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
 github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
 github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
@@ -920,6 +985,8 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
 github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
 github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
 github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/stuart-warren/yamlfmt v0.1.2 h1:ojguhYdHpNWy62fLkrQtLFGAzrqFxVaU8f5Z0U8mkMI=
+github.com/stuart-warren/yamlfmt v0.1.2/go.mod h1:X5TuPH+hf4O0U1KBvNqygvHbvAnoi9Wyl9BbtPv8SZk=
 github.com/tencentcloud/tencentcloud-sdk-go v3.0.82+incompatible/go.mod h1:0PfYow01SHPMhKY31xa+EFz2RStxIqj6JFAJS+IkCi4=
 github.com/tencentyun/cos-go-sdk-v5 v0.0.0-20190808065407-f07404cefc8c/go.mod h1:wk2XFUg6egk4tSDNZtXeKfe2G6690UVyt163PuUxBZk=
 github.com/terraform-linters/tflint v0.47.0 h1:RTN4b4E+O//rUgWDgQtEJ+DN6893aUSjrsL8c7wgPag=
@@ -955,6 +1022,8 @@ github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17
 github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
 github.com/xiang90/probing v0.0.0-20160813154853-07dd2e8dfe18/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
 github.com/xlab/treeprint v0.0.0-20161029104018-1d6e34225557/go.mod h1:ce1O1j6UtZfjr22oyGxGLbauSBp2YVXpARAosm7dHBg=
+github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8=
+github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs=
 github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
 github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
 github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@@ -1004,6 +1073,7 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8U
 golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20191202143827-86a70503ff7e/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
@@ -1228,12 +1298,14 @@ golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -1252,6 +1324,9 @@ golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY=
+golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek=
@@ -1599,6 +1674,7 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
 gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
@@ -1625,3 +1701,5 @@ rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
 rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
 sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI=
 sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
+sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q=
+sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=
diff --git a/options/options.go b/options/options.go
index fb0b9c061e..02a7cdc4ba 100644
--- a/options/options.go
+++ b/options/options.go
@@ -246,8 +246,14 @@ type TerragruntOptions struct {
 	// Controls if s3 bucket should be updated or skipped
 	DisableBucketUpdate bool
 
-	// Disalbes validation terraform command
+	// Disables validation terraform command
 	DisableCommandValidation bool
+
+	// Variables for usage in scaffolding.
+	ScaffoldVars []string
+
+	// Files with variables to be used in modules scaffolding.
+	ScaffoldVarFiles []string
 }
 
 // IAMOptions represents options that are used by Terragrunt to assume an IAM role.
diff --git a/shell/run_shell_cmd.go b/shell/run_shell_cmd.go
index 9e13ca28a9..4adff5fccf 100644
--- a/shell/run_shell_cmd.go
+++ b/shell/run_shell_cmd.go
@@ -4,6 +4,7 @@ import (
 	"bytes"
 	"fmt"
 	"io"
+	"net/url"
 	"os"
 	"os/exec"
 	"os/signal"
@@ -12,6 +13,8 @@ import (
 	"syscall"
 	"time"
 
+	"github.com/hashicorp/go-version"
+
 	"golang.org/x/text/cases"
 	"golang.org/x/text/language"
 
@@ -30,6 +33,13 @@ import (
 // Since we cannot know how the signal is sent, we should give `terraform` time to gracefully exit if it receives the signal directly from the shell, to avoid sending the second interrupt signal to `terraform`.
 const signalForwardingDelay = time.Second * 30
 
+const (
+	gitPrefix = "git::"
+	refsTags  = "refs/tags/"
+
+	tagSplitPart = 2
+)
+
 // Commands that implement a REPL need a pseudo TTY when run as a subprocess in order for the readline properties to be
 // preserved. This is a list of terraform commands that have this property, which is used to determine if terragrunt
 // should allocate a ptty when running that terraform command.
@@ -295,6 +305,78 @@ func GitTopLevelDir(terragruntOptions *options.TerragruntOptions, path string) (
 	return strings.TrimSpace(cmd.Stdout), nil
 }
 
+// GitRepoTags - fetch git repository tags from passed url
+func GitRepoTags(opts *options.TerragruntOptions, gitRepo *url.URL) ([]string, error) {
+	repoPath := gitRepo.String()
+	// remove git:: part if present
+	repoPath = strings.TrimPrefix(repoPath, gitPrefix)
+
+	stdout := bytes.Buffer{}
+	stderr := bytes.Buffer{}
+	gitOpts, err := options.NewTerragruntOptionsWithConfigPath(opts.WorkingDir)
+	if err != nil {
+		return nil, err
+	}
+	gitOpts.Env = opts.Env
+	gitOpts.Writer = &stdout
+	gitOpts.ErrWriter = &stderr
+
+	output, err := RunShellCommandWithOutput(opts, opts.WorkingDir, true, false, "git", "ls-remote", "--tags", repoPath)
+	if err != nil {
+		return nil, errors.WithStackTrace(err)
+	}
+	var tags []string
+	tagLines := strings.Split(output.Stdout, "\n")
+	for _, line := range tagLines {
+		fields := strings.Fields(line)
+		if len(fields) >= tagSplitPart {
+			tags = append(tags, fields[1])
+		}
+	}
+	return tags, nil
+}
+
+// GitLastReleaseTag - fetch git repository last release tag
+func GitLastReleaseTag(opts *options.TerragruntOptions, gitRepo *url.URL) (string, error) {
+	tags, err := GitRepoTags(opts, gitRepo)
+	if err != nil {
+		return "", err
+	}
+	if len(tags) == 0 {
+		return "", nil
+	}
+	return lastReleaseTag(tags), nil
+}
+
+// lastReleaseTag - return last release tag from passed tags slice.
+func lastReleaseTag(tags []string) string {
+	semverTags := extractSemVerTags(tags)
+	if len(semverTags) == 0 {
+		return ""
+	}
+	// find last semver tag
+	lastVersion := semverTags[0]
+	for _, ver := range semverTags {
+		if ver.GreaterThanOrEqual(lastVersion) {
+			lastVersion = ver
+		}
+	}
+	return lastVersion.Original()
+}
+
+// extractSemVerTags - extract semver tags from passed tags slice.
+func extractSemVerTags(tags []string) []*version.Version {
+	var semverTags []*version.Version
+	for _, tag := range tags {
+		t := strings.TrimPrefix(tag, refsTags)
+		if v, err := version.NewVersion(t); err == nil {
+			// consider only semver tags
+			semverTags = append(semverTags, v)
+		}
+	}
+	return semverTags
+}
+
 // ProcessExecutionError - error returned when a command fails, contains StdOut and StdErr
 type ProcessExecutionError struct {
 	Err        error
diff --git a/terraform/source.go b/terraform/source.go
index 1c9477cdda..6a844e9958 100644
--- a/terraform/source.go
+++ b/terraform/source.go
@@ -145,12 +145,12 @@ func NewSource(source string, downloadDir string, workingDir string, logger *log
 		return nil, err
 	}
 
-	canonicalSourceUrl, err := toSourceUrl(source, canonicalWorkingDir)
+	canonicalSourceUrl, err := ToSourceUrl(source, canonicalWorkingDir)
 	if err != nil {
 		return nil, err
 	}
 
-	rootSourceUrl, modulePath, err := splitSourceUrl(canonicalSourceUrl, logger)
+	rootSourceUrl, modulePath, err := SplitSourceUrl(canonicalSourceUrl, logger)
 	if err != nil {
 		return nil, err
 	}
@@ -187,7 +187,7 @@ func NewSource(source string, downloadDir string, workingDir string, logger *log
 
 // Convert the given source into a URL struct. This method should be able to handle all source URLs that the terraform
 // init command can handle, parsing local file paths, Git paths, and HTTP URLs correctly.
-func toSourceUrl(source string, workingDir string) (*url.URL, error) {
+func ToSourceUrl(source string, workingDir string) (*url.URL, error) {
 	// The go-getter library is what Terraform's init command uses to download source URLs. Use that library to
 	// parse the URL.
 	rawSourceUrlWithGetter, err := getter.Detect(source, workingDir, getter.Detectors)
@@ -234,7 +234,7 @@ func IsLocalSource(sourceUrl *url.URL) bool {
 // (//), which typically represents the root of a modules repo (e.g. github.com/foo/infrastructure-modules) and the
 // path is everything after the double slash. If there is no double-slash in the URL, the root repo is the entire
 // sourceUrl and the path is an empty string.
-func splitSourceUrl(sourceUrl *url.URL, logger *logrus.Entry) (*url.URL, string, error) {
+func SplitSourceUrl(sourceUrl *url.URL, logger *logrus.Entry) (*url.URL, string, error) {
 	pathSplitOnDoubleSlash := strings.SplitN(sourceUrl.Path, "//", 2)
 
 	if len(pathSplitOnDoubleSlash) > 1 {
diff --git a/util/file.go b/util/file.go
index 8ac9237fc3..abcc5c27ff 100644
--- a/util/file.go
+++ b/util/file.go
@@ -18,9 +18,12 @@ import (
 	"github.com/sirupsen/logrus"
 )
 
-const TerraformLockFile = ".terraform.lock.hcl"
-
-const TerragruntCacheDir = ".terragrunt-cache"
+const (
+	TerraformLockFile     = ".terraform.lock.hcl"
+	TerragruntCacheDir    = ".terragrunt-cache"
+	DefaultBoilerplateDir = ".boilerplate"
+	TfFileExtension       = ".tf"
+)
 
 // FileOrData will read the contents of the data of the given arg if it is a file, and otherwise return the contents by
 // itself. This will return an error if the given path is a directory.
@@ -592,3 +595,35 @@ func CopyLockFile(sourceFolder string, destinationFolder string, logger *logrus.
 
 	return nil
 }
+
+// ListTfFiles returns a list of all TF files in the specified directory.
+func ListTfFiles(directoryPath string) ([]string, error) {
+	var tfFiles []string
+
+	err := filepath.Walk(directoryPath, func(path string, info os.FileInfo, err error) error {
+		if err != nil {
+			return err
+		}
+		if !info.IsDir() && filepath.Ext(path) == TfFileExtension {
+			tfFiles = append(tfFiles, path)
+		}
+		return nil
+	})
+
+	return tfFiles, err
+}
+
+// IsDirectoryEmpty - returns true if the given path exists and is a empty directory.
+func IsDirectoryEmpty(dirPath string) (bool, error) {
+	dir, err := os.Open(dirPath)
+	if err != nil {
+		return false, err
+	}
+	defer dir.Close()
+
+	_, err = dir.Readdir(1)
+	if err == nil {
+		return false, nil
+	}
+	return true, nil
+}


Test patch:
diff --git a/cli/app_test.go b/cli/app_test.go
index 6135c05884..df59df36de 100644
--- a/cli/app_test.go
+++ b/cli/app_test.go
@@ -476,7 +476,7 @@ func TestAutocomplete(t *testing.T) {
 	}{
 		{
 			"",
-			[]string{"aws-provider-patch", "graph-dependencies", "hclfmt", "output-module-groups", "render-json", "run-all", "terragrunt-info", "validate-inputs"},
+			[]string{"aws-provider-patch", "graph-dependencies", "hclfmt", "output-module-groups", "render-json", "run-all", "scaffold", "terragrunt-info", "validate-inputs"},
 		},
 		{
 			"--versio",
diff --git a/cli/commands/scaffold/action_test.go b/cli/commands/scaffold/action_test.go
new file mode 100644
index 0000000000..a98b3f9e43
--- /dev/null
+++ b/cli/commands/scaffold/action_test.go
@@ -0,0 +1,92 @@
+package scaffold
+
+import (
+	"os"
+	"path/filepath"
+	"testing"
+
+	boilerplateoptions "github.com/gruntwork-io/boilerplate/options"
+	"github.com/gruntwork-io/boilerplate/templates"
+	"github.com/gruntwork-io/boilerplate/variables"
+	"github.com/gruntwork-io/terragrunt/config"
+	"github.com/gruntwork-io/terragrunt/options"
+	"github.com/gruntwork-io/terragrunt/util"
+	"github.com/stretchr/testify/require"
+)
+
+func TestDefaultTemplateVariables(t *testing.T) {
+	t.Parallel()
+
+	// set pre-defined variables
+	vars := map[string]interface{}{}
+	var requiredVariables, optionalVariables []*config.ParsedVariable
+
+	requiredVariables = append(requiredVariables, &config.ParsedVariable{
+		Name:                    "required_var_1",
+		Description:             "required_var_1 description",
+		Type:                    "string",
+		DefaultValuePlaceholder: "\"\"",
+	})
+
+	optionalVariables = append(optionalVariables, &config.ParsedVariable{
+		Name:         "optional_var_2",
+		Description:  "optional_ver_2 description",
+		Type:         "number",
+		DefaultValue: "42",
+	})
+
+	vars["requiredVariables"] = requiredVariables
+	vars["optionalVariables"] = optionalVariables
+
+	vars["sourceUrl"] = "git::https://github.com/gruntwork-io/terragrunt.git//test/fixture-inputs?ref=v0.53.8"
+
+	vars["EnableRootInclude"] = false
+
+	workDir := t.TempDir()
+	templateDir := util.JoinPath(workDir, "template")
+	err := os.Mkdir(templateDir, 0755)
+	require.NoError(t, err)
+
+	outputDir := util.JoinPath(workDir, "output")
+	err = os.Mkdir(outputDir, 0755)
+	require.NoError(t, err)
+
+	err = os.WriteFile(util.JoinPath(templateDir, "terragrunt.hcl"), []byte(defaultTerragruntTemplate), 0644)
+	require.NoError(t, err)
+
+	err = os.WriteFile(util.JoinPath(templateDir, "boilerplate.yml"), []byte(defaultBoilerplateConfig), 0644)
+	require.NoError(t, err)
+
+	boilerplateOpts := &boilerplateoptions.BoilerplateOptions{
+		OutputFolder:    outputDir,
+		OnMissingKey:    boilerplateoptions.DefaultMissingKeyAction,
+		OnMissingConfig: boilerplateoptions.DefaultMissingConfigAction,
+		Vars:            vars,
+		DisableShell:    true,
+		DisableHooks:    true,
+		NonInteractive:  true,
+		TemplateFolder:  templateDir,
+	}
+
+	emptyDep := variables.Dependency{}
+	err = templates.ProcessTemplate(boilerplateOpts, boilerplateOpts, emptyDep)
+	require.NoError(t, err)
+
+	content, err := util.ReadFileAsString(filepath.Join(outputDir, "terragrunt.hcl"))
+	require.NoError(t, err)
+	require.Contains(t, content, "required_var_1")
+	require.Contains(t, content, "optional_var_2")
+
+	// read generated HCL file and check if it is parsed correctly
+	opts, err := options.NewTerragruntOptionsForTest(filepath.Join(outputDir, "terragrunt.hcl"))
+	require.NoError(t, err)
+
+	cfg, err := config.ReadTerragruntConfig(opts)
+	require.NoError(t, err)
+	require.NotEmpty(t, cfg.Inputs)
+	require.Equal(t, 1, len(cfg.Inputs))
+	_, found := cfg.Inputs["required_var_1"]
+	require.True(t, found)
+	require.Equal(t, "git::https://github.com/gruntwork-io/terragrunt.git//test/fixture-inputs?ref=v0.53.8", *cfg.Terraform.Source)
+
+}
diff --git a/config/variable_test.go b/config/variable_test.go
new file mode 100644
index 0000000000..6139a3c16e
--- /dev/null
+++ b/config/variable_test.go
@@ -0,0 +1,81 @@
+package config
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestScanVariables(t *testing.T) {
+	t.Parallel()
+
+	opts := terragruntOptionsForTest(t, "")
+
+	inputs, err := ParseVariables(opts, "../test/fixture-inputs")
+	assert.NoError(t, err)
+	assert.Len(t, inputs, 11)
+
+	varByName := map[string]*ParsedVariable{}
+	for _, input := range inputs {
+		varByName[input.Name] = input
+	}
+
+	assert.Equal(t, "string", varByName["string"].Type)
+	assert.Equal(t, "\"\"", varByName["string"].DefaultValuePlaceholder)
+
+	assert.Equal(t, "bool", varByName["bool"].Type)
+	assert.Equal(t, "false", varByName["bool"].DefaultValuePlaceholder)
+
+	assert.Equal(t, "number", varByName["number"].Type)
+	assert.Equal(t, "0", varByName["number"].DefaultValuePlaceholder)
+
+	assert.Equal(t, "object", varByName["object"].Type)
+	assert.Equal(t, "{}", varByName["object"].DefaultValuePlaceholder)
+
+	assert.Equal(t, "map", varByName["map_bool"].Type)
+	assert.Equal(t, "{}", varByName["map_bool"].DefaultValuePlaceholder)
+
+	assert.Equal(t, "list", varByName["list_bool"].Type)
+	assert.Equal(t, "[]", varByName["list_bool"].DefaultValuePlaceholder)
+}
+
+func TestScanDefaultVariables(t *testing.T) {
+	t.Parallel()
+	opts := terragruntOptionsForTest(t, "")
+
+	inputs, err := ParseVariables(opts, "../test/fixture-inputs-defaults")
+	assert.NoError(t, err)
+	assert.Len(t, inputs, 11)
+
+	varByName := map[string]*ParsedVariable{}
+	for _, input := range inputs {
+		varByName[input.Name] = input
+	}
+
+	assert.Equal(t, "string", varByName["project_name"].Type)
+	assert.Equal(t, "Project name", varByName["project_name"].Description)
+	assert.Equal(t, "\"\"", varByName["project_name"].DefaultValuePlaceholder)
+
+	assert.Equal(t, "(variable no_type_value_var does not define a type)", varByName["no_type_value_var"].Type)
+	assert.Equal(t, "(variable no_type_value_var did not define a description)", varByName["no_type_value_var"].Description)
+	assert.Equal(t, "\"\"", varByName["no_type_value_var"].DefaultValuePlaceholder)
+
+	assert.Equal(t, "number", varByName["number_default"].Type)
+	assert.Equal(t, "number variable with default", varByName["number_default"].Description)
+	assert.Equal(t, "42", varByName["number_default"].DefaultValue)
+	assert.Equal(t, "0", varByName["number_default"].DefaultValuePlaceholder)
+
+	assert.Equal(t, "object", varByName["object_var"].Type)
+	assert.Equal(t, "{\"num\":42,\"str\":\"default\"}", varByName["object_var"].DefaultValue)
+
+	assert.Equal(t, "map", varByName["map_var"].Type)
+	assert.Equal(t, "{\"key\":\"value42\"}", varByName["map_var"].DefaultValue)
+
+	assert.Equal(t, "bool", varByName["enabled"].Type)
+	assert.Equal(t, "true", varByName["enabled"].DefaultValue)
+	assert.Equal(t, "Enable or disable the module", varByName["enabled"].Description)
+
+	assert.Equal(t, "string", varByName["vpc"].Type)
+	assert.Equal(t, "\"default-vpc\"", varByName["vpc"].DefaultValue)
+	assert.Equal(t, "VPC to be used", varByName["vpc"].Description)
+}
diff --git a/shell/run_shell_cmd_test.go b/shell/run_shell_cmd_test.go
index 5b89628d5b..5ae8120d8e 100644
--- a/shell/run_shell_cmd_test.go
+++ b/shell/run_shell_cmd_test.go
@@ -53,3 +53,19 @@ func TestRunShellOutputToStderrAndStdout(t *testing.T) {
 	assert.True(t, strings.Contains(stderr.String(), "Terraform"), "Output directed to stderr")
 	assert.True(t, len(stdout.String()) == 0, "No output to stdout")
 }
+
+func TestLastReleaseTag(t *testing.T) {
+	t.Parallel()
+	var tags = []string{
+		"refs/tags/v0.0.1",
+		"refs/tags/v0.0.2",
+		"refs/tags/v0.10.0",
+		"refs/tags/v20.0.1",
+		"refs/tags/v0.3.1",
+		"refs/tags/v20.1.2",
+		"refs/tags/v0.5.1",
+	}
+	lastTag := lastReleaseTag(tags)
+	assert.NotEmpty(t, lastTag)
+	assert.Equal(t, "v20.1.2", lastTag)
+}
diff --git a/terraform/source_test.go b/terraform/source_test.go
index 078e139a91..6a7c52520d 100644
--- a/terraform/source_test.go
+++ b/terraform/source_test.go
@@ -51,7 +51,7 @@ func TestSplitSourceUrl(t *testing.T) {
 			terragruntOptions, err := options.NewTerragruntOptionsForTest("testing")
 			require.NoError(t, err)
 
-			actualRootRepo, actualModulePath, err := splitSourceUrl(sourceUrl, terragruntOptions.Logger)
+			actualRootRepo, actualModulePath, err := SplitSourceUrl(sourceUrl, terragruntOptions.Logger)
 			require.NoError(t, err)
 
 			assert.Equal(t, testCase.expectedRootRepo, actualRootRepo.String())
@@ -67,11 +67,11 @@ func TestRegressionSupportForGitRemoteCodecommit(t *testing.T) {
 	require.NoError(t, err)
 
 	source := "git::codecommit::ap-northeast-1://my_app_modules//my-app/modules/main-module"
-	sourceURL, err := toSourceUrl(source, ".")
+	sourceURL, err := ToSourceUrl(source, ".")
 	require.NoError(t, err)
 	require.Equal(t, "git::codecommit::ap-northeast-1", sourceURL.Scheme)
 
-	actualRootRepo, actualModulePath, err := splitSourceUrl(sourceURL, terragruntOptions.Logger)
+	actualRootRepo, actualModulePath, err := SplitSourceUrl(sourceURL, terragruntOptions.Logger)
 	require.NoError(t, err)
 
 	require.Equal(t, "git::codecommit::ap-northeast-1://my_app_modules", actualRootRepo.String())
diff --git a/test/fixture-inputs-defaults/main.tf b/test/fixture-inputs-defaults/main.tf
new file mode 100644
index 0000000000..0e62abccdb
--- /dev/null
+++ b/test/fixture-inputs-defaults/main.tf
@@ -0,0 +1,66 @@
+variable "project_name" {
+  type        = string
+  description = "Project name"
+
+}
+
+variable "open_port" {
+  type        = number
+  description = "Port to open"
+}
+
+variable "enable_backups" {
+  type = bool
+}
+
+variable "no_type_value_var" {}
+
+
+variable "default_var" {
+  default = {
+    x = 1
+  }
+}
+
+variable "number_default" {
+  type        = number
+  default     = 42
+  description = "number variable with default"
+}
+
+variable "object_var" {
+  type = object({
+    str = string
+    num = number
+  })
+
+  default = {
+    str = "default"
+    num = 42
+  }
+}
+
+variable "map_var" {
+  type = map(string)
+  default = {
+    key = "value42"
+  }
+}
+
+variable "list_var" {
+  type    = list(number)
+  default = [1, 2, 3]
+}
+
+variable "enabled" {
+  description = "Enable or disable the module"
+  type        = bool
+  default     = true
+}
+
+variable "vpc" {
+  type        = string
+  description = "VPC to be used"
+  default     = "default-vpc"
+
+}
diff --git a/test/fixture-scaffold/external-template/boilerplate.yml b/test/fixture-scaffold/external-template/boilerplate.yml
new file mode 100644
index 0000000000..870ee34aca
--- /dev/null
+++ b/test/fixture-scaffold/external-template/boilerplate.yml
@@ -0,0 +1,2 @@
+# boilerplate config
+variables:
diff --git a/test/fixture-scaffold/external-template/external-template.txt b/test/fixture-scaffold/external-template/external-template.txt
new file mode 100644
index 0000000000..833a7d03b0
--- /dev/null
+++ b/test/fixture-scaffold/external-template/external-template.txt
@@ -0,0 +1,1 @@
+# External template
\ No newline at end of file
diff --git a/test/fixture-scaffold/external-template/terragrunt.hcl b/test/fixture-scaffold/external-template/terragrunt.hcl
new file mode 100644
index 0000000000..9b2e89089b
--- /dev/null
+++ b/test/fixture-scaffold/external-template/terragrunt.hcl
@@ -0,0 +1,9 @@
+# Project template dir
+terraform {
+  source = "{{ .sourceUrl }}"
+}
+
+inputs = {
+  project_name = "template"
+
+}
\ No newline at end of file
diff --git a/test/fixture-scaffold/module-with-template/.boilerplate/boilerplate.yml b/test/fixture-scaffold/module-with-template/.boilerplate/boilerplate.yml
new file mode 100644
index 0000000000..870ee34aca
--- /dev/null
+++ b/test/fixture-scaffold/module-with-template/.boilerplate/boilerplate.yml
@@ -0,0 +1,2 @@
+# boilerplate config
+variables:
diff --git a/test/fixture-scaffold/module-with-template/.boilerplate/template-file.txt b/test/fixture-scaffold/module-with-template/.boilerplate/template-file.txt
new file mode 100644
index 0000000000..5e32e73b47
--- /dev/null
+++ b/test/fixture-scaffold/module-with-template/.boilerplate/template-file.txt
@@ -0,0 +1,1 @@
+# Template file
\ No newline at end of file
diff --git a/test/fixture-scaffold/module-with-template/.boilerplate/terragrunt.hcl b/test/fixture-scaffold/module-with-template/.boilerplate/terragrunt.hcl
new file mode 100644
index 0000000000..9b2e89089b
--- /dev/null
+++ b/test/fixture-scaffold/module-with-template/.boilerplate/terragrunt.hcl
@@ -0,0 +1,9 @@
+# Project template dir
+terraform {
+  source = "{{ .sourceUrl }}"
+}
+
+inputs = {
+  project_name = "template"
+
+}
\ No newline at end of file
diff --git a/test/fixture-scaffold/module-with-template/main.tf b/test/fixture-scaffold/module-with-template/main.tf
new file mode 100644
index 0000000000..e75a5771a1
--- /dev/null
+++ b/test/fixture-scaffold/module-with-template/main.tf
@@ -0,0 +1,21 @@
+
+variable "project_name" {
+  type        = string
+  description = "Project name"
+}
+
+variable "vpc" {
+  type        = string
+  description = "VPC to be used"
+  default     = "default-vpc"
+}
+
+variable "replica_count" {
+  type    = number
+  default = 666
+}
+
+resource "local_file" "config" {
+  content  = "${var.project_name} vpc: ${var.vpc} replica_count: ${var.replica_count}"
+  filename = "config.txt"
+}
diff --git a/test/fixture-scaffold/scaffold-module/main.tf b/test/fixture-scaffold/scaffold-module/main.tf
new file mode 100644
index 0000000000..25113a8762
--- /dev/null
+++ b/test/fixture-scaffold/scaffold-module/main.tf
@@ -0,0 +1,5 @@
+
+resource "local_file" "config" {
+  content  = "${var.project_name} vpc: ${var.vpc} replica_count: ${var.replica_count}"
+  filename = "config.txt"
+}
diff --git a/test/fixture-scaffold/scaffold-module/variables.tf b/test/fixture-scaffold/scaffold-module/variables.tf
new file mode 100644
index 0000000000..21e0b26477
--- /dev/null
+++ b/test/fixture-scaffold/scaffold-module/variables.tf
@@ -0,0 +1,54 @@
+variable "project_name" {
+  type        = string
+  description = "Project name"
+}
+
+variable "vpc" {
+  type        = string
+  description = "VPC to be used"
+  default     = "default-vpc"
+}
+
+variable "replica_count" {
+  type    = number
+  default = 666
+}
+
+variable "enabled" {
+  description = "Enable or disable the module"
+  type        = bool
+  default     = true
+}
+
+variable "open_port" {
+  type        = number
+  description = "Port to open"
+}
+
+variable "enable_backups" {
+  type = bool
+}
+
+variable "users" {
+  type        = list(string)
+  description = "List of users"
+}
+
+variable "policy_map" {
+  type        = map(string)
+  description = "Map of policies"
+}
+
+variable "test_1" {}
+
+variable "test_2" {
+  default = {
+    x = 1
+  }
+}
+
+variable "test_3" {
+  type        = number
+  default     = 666
+  description = "description test 3"
+}
diff --git a/test/integration_scaffold_test.go b/test/integration_scaffold_test.go
new file mode 100644
index 0000000000..1033e323b0
--- /dev/null
+++ b/test/integration_scaffold_test.go
@@ -0,0 +1,191 @@
+package test
+
+import (
+	"bytes"
+	"fmt"
+	"os"
+	"path/filepath"
+	"testing"
+
+	"github.com/gruntwork-io/terragrunt/util"
+
+	"github.com/stretchr/testify/require"
+)
+
+const (
+	// TODO: change ref values to master once the feature is merged
+	TEST_SCAFOLD_MODULE                   = "https://github.com/gruntwork-io/terragrunt.git//test/fixture-scaffold/scaffold-module?ref=feature/scaffold"
+	TEST_SCAFOLD_MODULE_GIT               = "git@github.com:gruntwork-io/terragrunt.git//test/fixture-scaffold/scaffold-module?ref=feature/scaffold"
+	TEST_SCAFOLD_MODULE_SHORT             = "github.com/gruntwork-io/terragrunt.git//test/fixture-inputs"
+	TEST_SCAFOLD_TEMPLATE_MODULE          = "git@github.com:gruntwork-io/terragrunt.git//test/fixture-scaffold/module-with-template?ref=feature/scaffold"
+	TEST_SCAFOLD_EXTERNAL_TEMPLATE_MODULE = "git@github.com:gruntwork-io/terragrunt.git//test/fixture-scaffold/external-template?ref=feature/scaffold"
+)
+
+func TestTerragruntScaffoldModule(t *testing.T) {
+	t.Parallel()
+
+	tmpEnvPath, err := os.MkdirTemp("", "terragrunt-scaffold-test")
+	require.NoError(t, err)
+
+	stdout := bytes.Buffer{}
+	stderr := bytes.Buffer{}
+
+	err = runTerragruntCommand(t, fmt.Sprintf("terragrunt --terragrunt-non-interactive --terragrunt-working-dir %s scaffold %s", tmpEnvPath, TEST_SCAFOLD_MODULE), &stdout, &stderr)
+	require.NoError(t, err)
+	require.Contains(t, stderr.String(), "Scaffolding completed")
+	require.FileExists(t, fmt.Sprintf("%s/terragrunt.hcl", tmpEnvPath))
+}
+
+func TestTerragruntScaffoldModuleShortUrl(t *testing.T) {
+	t.Parallel()
+
+	tmpEnvPath, err := os.MkdirTemp("", "terragrunt-scaffold-test")
+	require.NoError(t, err)
+
+	stdout := bytes.Buffer{}
+	stderr := bytes.Buffer{}
+
+	err = runTerragruntCommand(t, fmt.Sprintf("terragrunt --terragrunt-non-interactive --terragrunt-working-dir %s scaffold %s", tmpEnvPath, TEST_SCAFOLD_MODULE_SHORT), &stdout, &stderr)
+	require.NoError(t, err)
+	require.Contains(t, stderr.String(), "Scaffolding completed")
+	// check that find_in_parent_folders is generated in terragrunt.hcl
+	content, err := util.ReadFileAsString(fmt.Sprintf("%s/terragrunt.hcl", tmpEnvPath))
+	require.NoError(t, err)
+	require.Contains(t, content, "find_in_parent_folders")
+}
+
+func TestTerragruntScaffoldModuleShortUrlNoRootInclude(t *testing.T) {
+	t.Parallel()
+
+	tmpEnvPath, err := os.MkdirTemp("", "terragrunt-scaffold-test")
+	require.NoError(t, err)
+
+	stdout := bytes.Buffer{}
+	stderr := bytes.Buffer{}
+
+	err = runTerragruntCommand(t, fmt.Sprintf("terragrunt --terragrunt-non-interactive --terragrunt-working-dir %s scaffold %s --var=EnableRootInclude=false", tmpEnvPath, TEST_SCAFOLD_MODULE_SHORT), &stdout, &stderr)
+	require.NoError(t, err)
+	require.Contains(t, stderr.String(), "Scaffolding completed")
+	// check that find_in_parent_folders is NOT generated in  terragrunt.hcl
+	content, err := util.ReadFileAsString(fmt.Sprintf("%s/terragrunt.hcl", tmpEnvPath))
+	require.NoError(t, err)
+	require.NotContains(t, content, "find_in_parent_folders")
+}
+
+func TestTerragruntScaffoldModuleDifferentRevision(t *testing.T) {
+	t.Parallel()
+
+	tmpEnvPath, err := os.MkdirTemp("", "terragrunt-scaffold-test")
+	require.NoError(t, err)
+
+	stdout := bytes.Buffer{}
+	stderr := bytes.Buffer{}
+
+	err = runTerragruntCommand(t, fmt.Sprintf("terragrunt --terragrunt-non-interactive --terragrunt-working-dir %s scaffold %s --var=Ref=v0.53.1", tmpEnvPath, TEST_SCAFOLD_MODULE_SHORT), &stdout, &stderr)
+
+	require.NoError(t, err)
+	require.Contains(t, stderr.String(), "git::https://github.com/gruntwork-io/terragrunt.git//test/fixture-inputs?ref=v0.53.1")
+	require.Contains(t, stderr.String(), "Scaffolding completed")
+}
+
+func TestTerragruntScaffoldModuleDifferentRevisionAndSsh(t *testing.T) {
+	t.Parallel()
+
+	tmpEnvPath, err := os.MkdirTemp("", "terragrunt-scaffold-test")
+	require.NoError(t, err)
+
+	stdout := bytes.Buffer{}
+	stderr := bytes.Buffer{}
+
+	err = runTerragruntCommand(t, fmt.Sprintf("terragrunt --terragrunt-non-interactive --terragrunt-working-dir %s scaffold %s --var=Ref=v0.53.1 --var=SourceUrlType=git-ssh", tmpEnvPath, TEST_SCAFOLD_MODULE_SHORT), &stdout, &stderr)
+	require.NoError(t, err)
+	require.Contains(t, stderr.String(), "git::ssh://git@github.com/gruntwork-io/terragrunt.git//test/fixture-inputs?ref=v0.53.1")
+	require.Contains(t, stderr.String(), "Scaffolding completed")
+}
+
+func TestTerragruntScaffoldModuleSsh(t *testing.T) {
+	t.Parallel()
+
+	tmpEnvPath, err := os.MkdirTemp("", "terragrunt-scaffold-test")
+	require.NoError(t, err)
+
+	stdout := bytes.Buffer{}
+	stderr := bytes.Buffer{}
+
+	err = runTerragruntCommand(t, fmt.Sprintf("terragrunt --terragrunt-non-interactive --terragrunt-working-dir %s scaffold %s", tmpEnvPath, TEST_SCAFOLD_MODULE_GIT), &stdout, &stderr)
+	require.NoError(t, err)
+	require.Contains(t, stderr.String(), "Scaffolding completed")
+}
+
+func TestTerragruntScaffoldModuleTemplate(t *testing.T) {
+	t.Parallel()
+
+	tmpEnvPath, err := os.MkdirTemp("", "terragrunt-scaffold-test")
+	require.NoError(t, err)
+
+	stdout := bytes.Buffer{}
+	stderr := bytes.Buffer{}
+
+	err = runTerragruntCommand(t, fmt.Sprintf("terragrunt --terragrunt-non-interactive --terragrunt-working-dir %s scaffold %s", tmpEnvPath, TEST_SCAFOLD_TEMPLATE_MODULE), &stdout, &stderr)
+	require.NoError(t, err)
+	require.Contains(t, stderr.String(), "Scaffolding completed")
+	// check that exists file from .boilerplate dir
+	require.FileExists(t, fmt.Sprintf("%s/template-file.txt", tmpEnvPath))
+}
+
+func TestTerragruntScaffoldModuleExternalTemplate(t *testing.T) {
+	t.Parallel()
+
+	tmpEnvPath, err := os.MkdirTemp("", "terragrunt-scaffold-test")
+	require.NoError(t, err)
+
+	stdout := bytes.Buffer{}
+	stderr := bytes.Buffer{}
+
+	err = runTerragruntCommand(t, fmt.Sprintf("terragrunt --terragrunt-non-interactive --terragrunt-working-dir %s scaffold %s %s", tmpEnvPath, TEST_SCAFOLD_MODULE_GIT, TEST_SCAFOLD_EXTERNAL_TEMPLATE_MODULE), &stdout, &stderr)
+	require.NoError(t, err)
+	require.Contains(t, stderr.String(), "Scaffolding completed")
+	// check that exists file from external template
+	require.FileExists(t, fmt.Sprintf("%s/external-template.txt", tmpEnvPath))
+}
+
+func TestTerragruntScaffoldErrorNoModuleUrl(t *testing.T) {
+	t.Parallel()
+
+	tmpEnvPath, err := os.MkdirTemp("", "terragrunt-scaffold-test")
+	require.NoError(t, err)
+
+	stdout := bytes.Buffer{}
+	stderr := bytes.Buffer{}
+
+	err = runTerragruntCommand(t, fmt.Sprintf("terragrunt --terragrunt-non-interactive --terragrunt-working-dir %s scaffold", tmpEnvPath), &stdout, &stderr)
+	require.Error(t, err)
+	require.Contains(t, err.Error(), "No module URL passed")
+}
+
+func TestTerragruntScaffoldModuleVarFile(t *testing.T) {
+	t.Parallel()
+	// generate var file with specific version, without root include and use GIT/SSH to clone module.
+	varFileContent := `
+Ref: v0.53.1
+EnableRootInclude: false
+SourceUrlType: "git-ssh"
+`
+	varFile := filepath.Join(t.TempDir(), "var-file.yaml")
+	err := os.WriteFile(varFile, []byte(varFileContent), 0644)
+	require.NoError(t, err)
+
+	tmpEnvPath, err := os.MkdirTemp("", "terragrunt-scaffold-test")
+	require.NoError(t, err)
+
+	stdout := bytes.Buffer{}
+	stderr := bytes.Buffer{}
+
+	err = runTerragruntCommand(t, fmt.Sprintf("terragrunt --terragrunt-non-interactive --terragrunt-working-dir %s scaffold %s --var-file=%s", tmpEnvPath, TEST_SCAFOLD_MODULE_SHORT, varFile), &stdout, &stderr)
+	require.NoError(t, err)
+	require.Contains(t, stderr.String(), "git::ssh://git@github.com/gruntwork-io/terragrunt.git//test/fixture-inputs?ref=v0.53.1")
+	require.Contains(t, stderr.String(), "Scaffolding completed")
+	content, err := util.ReadFileAsString(fmt.Sprintf("%s/terragrunt.hcl", tmpEnvPath))
+	require.NoError(t, err)
+	require.NotContains(t, content, "find_in_parent_folders")
+}
diff --git a/test/integration_test.go b/test/integration_test.go
index fc77510a23..e280ec11ad 100644
--- a/test/integration_test.go
+++ b/test/integration_test.go
@@ -6114,7 +6114,8 @@ func TestTerragruntPrintAwsErrors(t *testing.T) {
 	stderr := bytes.Buffer{}
 	err := runTerragruntCommand(t, fmt.Sprintf("terragrunt apply --terragrunt-non-interactive --terragrunt-config %s --terragrunt-working-dir %s", tmpTerragruntConfigFile, rootPath), &stdout, &stderr)
 	assert.Error(t, err)
-	assert.Contains(t, err.Error(), "BucketRegionError: incorrect region")
+	message := fmt.Sprintf("%v", err.Error())
+	assert.True(t, strings.Contains(message, "AllAccessDisabled: All access to this object has been disabled") || strings.Contains(message, "BucketRegionError: incorrect region"))
 }
 
 func TestTerragruntErrorWhenStateBucketIsInDifferentRegion(t *testing.T) {
diff --git a/util/file_test.go b/util/file_test.go
index e069e22fe3..0cb7960683 100644
--- a/util/file_test.go
+++ b/util/file_test.go
@@ -321,3 +321,20 @@ func TestIncludeInCopy(t *testing.T) {
 			"Unexpected copy result for file '%s' (should be copied: '%t') - got error: %s", testCase.path, testCase.copyExpected, err)
 	}
 }
+
+func TestEmptyDir(t *testing.T) {
+	t.Parallel()
+	testCases := []struct {
+		path        string
+		expectEmpty bool
+	}{
+		{t.TempDir(), true},
+		{os.TempDir(), false},
+	}
+	for _, testCase := range testCases {
+		emptyValue, err := IsDirectoryEmpty(testCase.path)
+		assert.NoError(t, err)
+		assert.Equal(t, testCase.expectEmpty, emptyValue, "For path %s", testCase.path)
+	}
+
+}