-
Notifications
You must be signed in to change notification settings - Fork 33
feat: Add optional function to return manifest of generated files #225
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -134,13 +134,14 @@ You can find older versions on the [Releases Page](https://github.com/gruntwork- | |
| you to run arbitrary scripts. | ||
| 1. **Cross-platform**: Boilerplate is easy to install (it's a standalone binary) and works on all major platforms (Mac, | ||
| Linux, Windows). | ||
| 1. **Versioned file manifest**: Optionally generate a versioned JSON manifest that tracks all created files across multiple template generations. Each time boilerplate runs with `--output-manifest`, it adds a new version entry to the existing manifest file (or creates one if none exists), preserving the history of all previous generations for integration with other tools. | ||
|
|
||
| ## Working with boilerplate | ||
|
|
||
| When you run Boilerplate, it performs the following steps: | ||
|
|
||
| 1. Read the `boilerplate.yml` file in the folder specified by the `--template-url` option to find all defined | ||
| varaibles. | ||
| variables. | ||
| 1. Gather values for the variables from any `--var` and `--var-file` options that were passed in and prompting the user | ||
| for the rest (unless the `--non-interactive` flag is specified). | ||
| 1. Copy each file from `--template-url` to `--output-folder`, running each non-binary file through the Go | ||
|
|
@@ -185,6 +186,7 @@ The `boilerplate` binary supports the following options: | |
| * `--no-shell`: If this flag is set, no `shell` helpers will execute. They will instead return the text "replace-me". | ||
| * `--disable-dependency-prompt` (optional): Do not prompt for confirmation to include dependencies. Has the same effect as | ||
| --non-interactive, without disabling variable prompts. Default: `false`. | ||
| * `--output-manifest` (optional): Write a versioned JSON manifest of all generated files to `boilerplate-manifest.json` in the output directory. If a manifest file already exists, a new version entry will be added to track the current generation alongside previous ones. | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would a better name for this flag be We don't require following these rules here, but if we can, let's go with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For clarity, are you suggesting we add multiple flags? The only problem I see with the latter is that we rely on a consistent file name for the history/version feature. If someone were to enter a file name with a typo you would lose your version history, or it would be incomplete the next time you run the command with the correct file name. |
||
| * `--help`: Show the help text and exit. | ||
| * `--version`: Show the version and exit. | ||
|
|
||
|
|
@@ -214,6 +216,12 @@ Generate a project in ~/output from the templates in this repo's `include` examp | |
| boilerplate --template-url "git@github.com:gruntwork-io/boilerplate.git//examples/for-learning-and-testing/include?ref=main" --output-folder ~/output --var-file vars.yml | ||
| ``` | ||
|
|
||
| Generate a project and create a versioned manifest file listing all generated files: | ||
|
|
||
| ``` | ||
| boilerplate --template-url ~/templates --output-folder ~/output --output-manifest | ||
| ``` | ||
|
|
||
|
|
||
| #### The boilerplate.yml file | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,6 +7,7 @@ import ( | |
| "github.com/gruntwork-io/go-commons/version" | ||
| "github.com/urfave/cli/v2" | ||
|
|
||
| "github.com/gruntwork-io/boilerplate/manifest" | ||
| "github.com/gruntwork-io/boilerplate/options" | ||
| "github.com/gruntwork-io/boilerplate/templates" | ||
| "github.com/gruntwork-io/boilerplate/variables" | ||
|
|
@@ -96,6 +97,10 @@ func CreateBoilerplateCli() *cli.App { | |
| Name: options.OptDisableDependencyPrompt, | ||
| Usage: fmt.Sprintf("Do not prompt for confirmation to include dependencies. Has the same effect as --%s, without disabling variable prompts.", options.OptNonInteractive), | ||
| }, | ||
| &cli.BoolFlag{ | ||
| Name: options.OptOutputManifest, | ||
| Usage: "Write a JSON list of all generated files to boilerplate-manifest.json in the current working directory.", | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Did I request that this be in JSON instead of YAML? Seems at odds with the configuration file being in YAML, but it is a more convenient format in general. Maybe the format should be configurable. If we can get the UX to be similar to the Run Report that might be better, so there's an expectation of parity in how the two behave. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That may have just been my default, this can be rewritten to yaml for sure. Will see how easy it is to make this configurable. Will have a look at Run Report. |
||
| }, | ||
| } | ||
|
|
||
| // We pass JSON/YAML content to various CLI flags, such as --var, and this JSON/YAML content may contain commas or | ||
|
|
@@ -123,5 +128,24 @@ func runApp(cliContext *cli.Context) error { | |
| // The root boilerplate.yml is not itself a dependency, so we pass an empty Dependency. | ||
| emptyDep := variables.Dependency{} | ||
|
|
||
| return templates.ProcessTemplate(opts, opts, emptyDep) | ||
| generatedFiles, err := templates.ProcessTemplate(opts, opts, emptyDep) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| if opts.OutputManifest { | ||
| // Convert file paths to GeneratedFile structs | ||
| files := make([]manifest.GeneratedFile, 0, len(generatedFiles)) | ||
| for _, path := range generatedFiles { | ||
| files = append(files, manifest.GeneratedFile{Path: path}) | ||
| } | ||
|
|
||
| // Update versioned manifest | ||
| err = manifest.UpdateVersionedManifest(opts.OutputFolder, opts.TemplateUrl, opts.Vars, files) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| } | ||
|
|
||
| return nil | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,7 +4,7 @@ | |
| package integration_tests | ||
|
|
||
| import ( | ||
| "io/ioutil" | ||
| "os" | ||
| "path/filepath" | ||
| "testing" | ||
|
|
||
|
|
@@ -19,9 +19,9 @@ func TestForProductionTerragruntArchitectureBoilerplateExample(t *testing.T) { | |
|
|
||
| forProductionExamplePath := "../examples/for-production/terragrunt-architecture-catalog" | ||
|
|
||
| outputBasePath, err := ioutil.TempDir("", "boilerplate-for-production-output") | ||
| outputBasePath, err := os.MkdirTemp("", "boilerplate-for-production-output") | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should replace this with |
||
| require.NoError(t, err) | ||
| //defer os.RemoveAll(outputBasePath) | ||
| defer os.RemoveAll(outputBasePath) | ||
|
|
||
| templateFolder, err := filepath.Abs(filepath.Join(forProductionExamplePath, "blueprints", "reference-architecture")) | ||
| require.NoError(t, err) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,13 +1,12 @@ | ||
| package integration_tests | ||
|
|
||
| import ( | ||
| "io/ioutil" | ||
| "os" | ||
| "testing" | ||
|
|
||
| "github.com/gruntwork-io/boilerplate/cli" | ||
| "github.com/gruntwork-io/boilerplate/config" | ||
| "github.com/gruntwork-io/go-commons/errors" | ||
| "github.com/gruntwork-io/boilerplate/errors" | ||
| "github.com/gruntwork-io/go-commons/version" | ||
| "github.com/stretchr/testify/assert" | ||
| "github.com/stretchr/testify/require" | ||
|
|
@@ -58,7 +57,7 @@ func TestRequiredVersionUnderTest(t *testing.T) { | |
| func runRequiredVersionExample(t *testing.T, templateFolder string) error { | ||
| app := cli.CreateBoilerplateCli() | ||
|
|
||
| outputPath, err := ioutil.TempDir("", "boilerplate-test-output-reqver") | ||
| outputPath, err := os.MkdirTemp("", "boilerplate-test-output-reqver") | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should replace this with |
||
| require.NoError(t, err) | ||
| defer os.RemoveAll(outputPath) | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,75 @@ | ||||||
| package manifest | ||||||
|
|
||||||
| import ( | ||||||
| "encoding/json" | ||||||
| "os" | ||||||
| "path/filepath" | ||||||
| "time" | ||||||
|
|
||||||
| "github.com/gruntwork-io/go-commons/version" | ||||||
| ) | ||||||
|
|
||||||
| // GeneratedFile represents a file that was generated by boilerplate | ||||||
| type GeneratedFile struct { | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does it make sense to also include the checksums of the files generated? |
||||||
| Path string `json:"path"` | ||||||
| } | ||||||
|
|
||||||
| // ManifestEntry represents a single version entry in the versioned manifest | ||||||
| type ManifestEntry struct { | ||||||
| Timestamp string `json:"timestamp"` | ||||||
| TemplateURL string `json:"template_url"` | ||||||
| BoilerplateVersion string `json:"boilerplate_version"` | ||||||
| Variables map[string]interface{} `json:"variables"` | ||||||
| OutputDir string `json:"output_dir"` | ||||||
| Files []GeneratedFile `json:"files"` | ||||||
| } | ||||||
|
|
||||||
| // VersionedManifest represents the complete versioned manifest | ||||||
| type VersionedManifest struct { | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This manifest file should include some information on the version of the manifest schema. That will be important if the schema changes. |
||||||
| LatestVersion string `json:"latest_version"` | ||||||
| Versions map[string]ManifestEntry `json:"versions"` | ||||||
| } | ||||||
|
|
||||||
|
|
||||||
|
|
||||||
| // UpdateVersionedManifest updates the versioned manifest with a new entry | ||||||
| func UpdateVersionedManifest(outputDir, templateURL string, variables map[string]interface{}, files []GeneratedFile) error { | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| manifestPath := filepath.Join(outputDir, "boilerplate-manifest.json") | ||||||
|
|
||||||
| // Read existing manifest or create new one | ||||||
| manifest := &VersionedManifest{ | ||||||
| Versions: make(map[string]ManifestEntry), | ||||||
| } | ||||||
|
|
||||||
| if data, err := os.ReadFile(manifestPath); err == nil { | ||||||
| if err := json.Unmarshal(data, manifest); err != nil { | ||||||
| // If unmarshal fails, start with empty manifest | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wait, why? The file is corrupted, and we should throw an error, right? |
||||||
| manifest = &VersionedManifest{ | ||||||
| Versions: make(map[string]ManifestEntry), | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| // Create new version entry | ||||||
| timestamp := time.Now().UTC().Format(time.RFC3339) | ||||||
| entry := ManifestEntry{ | ||||||
| Timestamp: timestamp, | ||||||
| TemplateURL: templateURL, | ||||||
| BoilerplateVersion: version.GetVersion(), | ||||||
| Variables: variables, | ||||||
| OutputDir: outputDir, | ||||||
| Files: files, | ||||||
| } | ||||||
|
|
||||||
| // Add to manifest | ||||||
| manifest.Versions[timestamp] = entry | ||||||
| manifest.LatestVersion = timestamp | ||||||
|
|
||||||
| // Write back to file | ||||||
| data, err := json.MarshalIndent(manifest, "", " ") | ||||||
| if err != nil { | ||||||
| return err | ||||||
| } | ||||||
|
|
||||||
| return os.WriteFile(manifestPath, data, 0644) | ||||||
| } | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| package manifest | ||
|
|
||
| import ( | ||
| "encoding/json" | ||
| "os" | ||
| "path/filepath" | ||
| "testing" | ||
| "time" | ||
|
|
||
| "github.com/stretchr/testify/assert" | ||
| "github.com/stretchr/testify/require" | ||
| ) | ||
|
|
||
| func TestUpdateVersionedManifestMultipleVersions(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| // Create temporary directory | ||
| tempDir, err := os.MkdirTemp("", "manifest-test") | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use |
||
| require.NoError(t, err) | ||
| defer os.RemoveAll(tempDir) | ||
|
|
||
| // First update | ||
| err = UpdateVersionedManifest(tempDir, "template1", map[string]interface{}{"var1": "value1"}, []GeneratedFile{{Path: "file1.txt"}}) | ||
| require.NoError(t, err) | ||
|
|
||
| // Wait a moment to ensure different timestamps | ||
| time.Sleep(time.Second * 1) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we really need this? |
||
|
|
||
| // Second update | ||
| err = UpdateVersionedManifest(tempDir, "template2", map[string]interface{}{"var2": "value2"}, []GeneratedFile{{Path: "file2.txt"}}) | ||
| require.NoError(t, err) | ||
|
|
||
| // Read manifest | ||
| manifestPath := filepath.Join(tempDir, "boilerplate-manifest.json") | ||
| data, err := os.ReadFile(manifestPath) | ||
| require.NoError(t, err) | ||
|
|
||
| var manifest VersionedManifest | ||
| err = json.Unmarshal(data, &manifest) | ||
| require.NoError(t, err) | ||
|
|
||
| // Verify we have 2 versions | ||
| assert.Len(t, manifest.Versions, 2) | ||
| assert.NotEmpty(t, manifest.LatestVersion) | ||
|
|
||
| // Verify latest version points to second update | ||
| latestEntry := manifest.Versions[manifest.LatestVersion] | ||
| assert.Equal(t, "template2", latestEntry.TemplateURL) | ||
| assert.Equal(t, map[string]interface{}{"var2": "value2"}, latestEntry.Variables) | ||
| } | ||
|
|
||
| func TestUpdateVersionedManifestInvalidExistingFile(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| // Create temporary directory | ||
| tempDir, err := os.MkdirTemp("", "manifest-test") | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use |
||
| require.NoError(t, err) | ||
| defer os.RemoveAll(tempDir) | ||
|
|
||
| // Create invalid JSON file | ||
| manifestPath := filepath.Join(tempDir, "boilerplate-manifest.json") | ||
| err = os.WriteFile(manifestPath, []byte("invalid json"), 0644) | ||
| require.NoError(t, err) | ||
|
|
||
| // Update should still work (creates new manifest) | ||
| err = UpdateVersionedManifest(tempDir, "template", map[string]interface{}{}, []GeneratedFile{}) | ||
| assert.NoError(t, err) | ||
|
|
||
| // Verify manifest was recreated | ||
| data, err := os.ReadFile(manifestPath) | ||
| require.NoError(t, err) | ||
|
|
||
| var manifest VersionedManifest | ||
| err = json.Unmarshal(data, &manifest) | ||
| assert.NoError(t, err) | ||
| assert.Len(t, manifest.Versions, 1) | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why are we preserving all previous versions?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I started there based on your previous comment: #225 (comment)
We could add some arbitrary version limit for sure, not sure what we'd consider reasonable though. I use boilerplate exclusively for scaffold, for which this isn't really a required feature. I just need the manifest to address the issue of terragrunt formatting the entire working directory. I can't really comment on other use cases of scaffold and/or what would be reasonable limits to version history.