Skip to content

Commit 811b2b5

Browse files
CLD-523: Add scaffolding (#349)
## CLD-523: Add scaffolding - Moves scaffolding to framework - Uses the projects root directory to determine the repository name in templates
1 parent 6639d28 commit 811b2b5

22 files changed

+713
-0
lines changed

.changeset/bitter-months-joke.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"chainlink-deployments-framework": minor
3+
---
4+
5+
Add scaffolding to the framework

engine/cld/scaffold/scaffold.go

Lines changed: 293 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,293 @@
1+
package scaffold
2+
3+
import (
4+
"embed"
5+
"fmt"
6+
"os"
7+
"os/exec"
8+
"path/filepath"
9+
"strings"
10+
"text/template"
11+
12+
cldf_domain "github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/domain"
13+
)
14+
15+
//go:embed templates/*
16+
var templates embed.FS
17+
18+
// ScaffoldOptions holds configuration options for domain scaffolding.
19+
type ScaffoldOptions struct {
20+
runModTidy bool
21+
}
22+
23+
// ScaffoldOption is a functional option type for configuring domain scaffolding.
24+
type ScaffoldOption func(*ScaffoldOptions)
25+
26+
// WithModTidy enables running 'go mod tidy' after scaffolding.
27+
// This will resolve and download all dependencies for the new domain.
28+
func WithModTidy() ScaffoldOption {
29+
return func(opts *ScaffoldOptions) {
30+
opts.runModTidy = true
31+
}
32+
}
33+
34+
// defaultScaffoldOptions returns the default configuration for domain scaffolding.
35+
func defaultScaffoldOptions() *ScaffoldOptions {
36+
return &ScaffoldOptions{
37+
runModTidy: false, // Don't run go mod tidy by default
38+
}
39+
}
40+
41+
// getRepositoryName extracts the repository name from the root directory path.
42+
func getRepositoryName(rootDir string) string {
43+
return filepath.Base(filepath.Dir(rootDir))
44+
}
45+
46+
// ScaffoldDomain creates a new domain directory structure within the specified base path.
47+
// Use WithModTidy() option to run 'go mod tidy' after scaffolding.
48+
func ScaffoldDomain(domain cldf_domain.Domain, opts ...ScaffoldOption) error {
49+
// Apply default options and then user-provided options
50+
options := defaultScaffoldOptions()
51+
for _, opt := range opts {
52+
opt(options)
53+
}
54+
55+
var err error
56+
57+
// Check if the directory already exists or if there is an error accessing it
58+
if err = checkDirExists(domain.DirPath()); err != nil {
59+
return fmt.Errorf("failed to create %s domain directory: %w", domain.String(), err)
60+
}
61+
62+
// Setup all the args for the templates
63+
renderArgs := map[string]string{
64+
"package": domain.Key(),
65+
"repo": getRepositoryName(domain.RootPath()),
66+
}
67+
68+
// Define the structure
69+
structure := dirFSNode(domain.Key(), []*fsnode{
70+
fileFSNode("go.mod", withTemplate("go.mod.tmpl")),
71+
dirFSNode("pkg", []*fsnode{gitKeepFSNode()}),
72+
dirFSNode(cldf_domain.LibDirName, []*fsnode{gitKeepFSNode()}),
73+
dirFSNode(cldf_domain.InternalDirName, []*fsnode{gitKeepFSNode()}),
74+
dirFSNode(cldf_domain.CmdDirName, []*fsnode{
75+
fileFSNode("main.go", withTemplate("cmd_main.go.tmpl")),
76+
dirFSNode(cldf_domain.InternalDirName, []*fsnode{
77+
dirFSNode("cli", []*fsnode{
78+
fileFSNode("app.go", withTemplate("cmd_internal_app.go.tmpl")),
79+
}),
80+
}),
81+
}),
82+
dirFSNode(".config", []*fsnode{
83+
dirFSNode("networks", []*fsnode{
84+
fileFSNode("mainnet.yaml", withTemplate("mainnet.yaml.tmpl")),
85+
fileFSNode("testnet.yaml", withTemplate("testnet.yaml.tmpl")),
86+
}),
87+
dirFSNode("local", []*fsnode{gitKeepFSNode()}),
88+
dirFSNode("ci", []*fsnode{
89+
fileFSNode("common.env", withTemplate("common.env.tmpl")),
90+
}),
91+
}),
92+
})
93+
94+
// Scaffold the domain structure
95+
if err := scaffold(structure, domain.RootPath(), renderArgs); err != nil {
96+
return err
97+
}
98+
99+
// Run go mod tidy in the newly created domain directory if requested
100+
if options.runModTidy {
101+
domainPath := domain.DirPath()
102+
if err := runGoModTidy(domainPath); err != nil {
103+
return fmt.Errorf("failed to run 'go mod tidy' in %s: %w", domainPath, err)
104+
}
105+
}
106+
107+
return nil
108+
}
109+
110+
// ScaffoldEnvDir creates a new environment directory structure within the specified base path.
111+
func ScaffoldEnvDir(envdir cldf_domain.EnvDir) error {
112+
// Check if the directory already exists or if there is an error accessing it
113+
if err := checkDirExists(envdir.DirPath()); err != nil {
114+
return fmt.Errorf("failed to create %s env directory: %w", envdir.String(), err)
115+
}
116+
117+
// Setup all the args for the templates
118+
renderArgs := map[string]string{
119+
"package": envdir.Key(),
120+
}
121+
122+
// Define the structure
123+
structure := dirFSNode(envdir.Key(), []*fsnode{
124+
fileFSNode(cldf_domain.AddressBookFileName, withTemplate("address_book.json.tmpl")),
125+
dirFSNode(cldf_domain.DatastoreDirName, []*fsnode{
126+
fileFSNode(cldf_domain.AddressRefsFileName, withTemplate("address_refs.json.tmpl")),
127+
fileFSNode(cldf_domain.ChainMetadataFileName, withTemplate("chain_metadata.json.tmpl")),
128+
fileFSNode(cldf_domain.ContractMetadataFileName, withTemplate("contract_metadata.json.tmpl")),
129+
fileFSNode(cldf_domain.EnvMetadataFileName, withTemplate("env_metadata.json.tmpl")),
130+
}),
131+
fileFSNode(cldf_domain.NodesFileName, withTemplate("nodes.json.tmpl")),
132+
fileFSNode(cldf_domain.ViewStateFileName, withTemplate("view_state.json.tmpl")),
133+
fileFSNode(cldf_domain.MigrationsFileName, withTemplate("migrations.go.tmpl")),
134+
fileFSNode("migrations_test.go", withTemplate("migrations_test.go.tmpl")),
135+
fileFSNode(cldf_domain.MigrationsArchiveFileName, withTemplate("migrations_archive.go.tmpl")),
136+
fileFSNode(cldf_domain.DurablePipelinesFileName, withTemplate("durable_pipelines.go.tmpl")),
137+
fileFSNode("durable_pipelines_test.go", withTemplate("durable_pipelines_test.go.tmpl")),
138+
dirFSNode(cldf_domain.ArtifactsDirName, []*fsnode{gitKeepFSNode()}),
139+
dirFSNode(cldf_domain.ProposalsDirName, []*fsnode{gitKeepFSNode()}),
140+
dirFSNode(cldf_domain.ArchivedProposalsDirName, []*fsnode{gitKeepFSNode()}),
141+
dirFSNode(cldf_domain.DecodedProposalsDirName, []*fsnode{gitKeepFSNode()}),
142+
dirFSNode(cldf_domain.OperationsReportsDirName, []*fsnode{gitKeepFSNode()}),
143+
dirFSNode(cldf_domain.DurablePipelineDirName, []*fsnode{
144+
gitKeepFSNode(),
145+
dirFSNode(cldf_domain.DurablePipelineInputsDirName, []*fsnode{gitKeepFSNode()}),
146+
}),
147+
})
148+
149+
return scaffold(structure, envdir.DomainDirPath(), renderArgs)
150+
}
151+
152+
// fsnode represents a file system node, which can be either a directory or a file.
153+
type fsnode struct {
154+
// name is the name of the file or directory.
155+
name string
156+
// isDir indicates whether this node is a directory or a file.
157+
isDir bool
158+
// children contains the child nodes of this node. It is only used if this node is a directory.
159+
children []*fsnode
160+
// templateName is the name of the template file that will be used to render the file. It is
161+
// only used if this node is a file.
162+
templateName string
163+
}
164+
165+
// scaffold walks the file system tree starting from the given root node and creates the
166+
// corresponding directories and files. It uses the provided basePath as the root directory
167+
// for the scaffolded structure. The renderArgs map is used to pass arguments to the template
168+
// rendering process for files that have a template associated with them.
169+
//
170+
// If an error occurs during the scaffolding process, the created directories and files will
171+
// be cleaned up to avoid leaving behind any partially created files or directories.
172+
func scaffold(root *fsnode, basePath string, renderArgs map[string]string) error {
173+
var err error
174+
175+
currentPath := filepath.Join(basePath, root.name)
176+
177+
// Clean up the directory if an error occurs
178+
defer func() {
179+
if err != nil {
180+
os.RemoveAll(currentPath)
181+
}
182+
}()
183+
184+
if root.isDir {
185+
if err = os.MkdirAll(currentPath, os.ModePerm); err != nil {
186+
return err
187+
}
188+
189+
for _, child := range root.children {
190+
if err := scaffold(child, currentPath, renderArgs); err != nil {
191+
return err
192+
}
193+
}
194+
} else {
195+
file, err := os.Create(currentPath)
196+
if err != nil {
197+
return err
198+
}
199+
defer file.Close()
200+
201+
if root.templateName != "" {
202+
content, err := renderTemplate(root.templateName, renderArgs)
203+
if err != nil {
204+
return err
205+
}
206+
207+
if _, err := file.WriteString(content); err != nil {
208+
return err
209+
}
210+
}
211+
}
212+
213+
return nil
214+
}
215+
216+
// dirFSNode constructs a directory fsnode with the given name and children.
217+
func dirFSNode(name string, children []*fsnode) *fsnode {
218+
return &fsnode{
219+
name: name,
220+
isDir: true,
221+
children: children,
222+
}
223+
}
224+
225+
// fileFSNode constructs a file fsnode with the given name.
226+
func fileFSNode(name string, opts ...func(*fsnode)) *fsnode {
227+
n := &fsnode{
228+
name: name,
229+
isDir: false,
230+
}
231+
232+
for _, opt := range opts {
233+
opt(n)
234+
}
235+
236+
return n
237+
}
238+
239+
// withTemplate sets the template name for the fsnode.
240+
func withTemplate(templateName string) func(*fsnode) {
241+
return func(n *fsnode) {
242+
n.templateName = templateName
243+
}
244+
}
245+
246+
// gitKeepFSNode constructs a .gitkeep file fsnode to keep empty directories in git. Useful since
247+
// we have many empty directories in the scaffolded structures.
248+
func gitKeepFSNode() *fsnode {
249+
return fileFSNode(".gitkeep")
250+
}
251+
252+
// renderTemplate renders a template with the given name and arguments.
253+
func renderTemplate(name string, renderArgs any) (string, error) {
254+
tmpls, err := template.New("").ParseFS(templates, "templates/*")
255+
if err != nil {
256+
return "", err
257+
}
258+
259+
b := &strings.Builder{}
260+
if err = tmpls.ExecuteTemplate(b, name, renderArgs); err != nil {
261+
return "", err
262+
}
263+
264+
return b.String(), nil
265+
}
266+
267+
// checkDirExists checks if the domain directory exists, returning os.ErrExist or any access error.
268+
func checkDirExists(path string) error {
269+
_, err := os.Stat(path)
270+
if err == nil {
271+
return os.ErrExist
272+
}
273+
274+
if !os.IsNotExist(err) {
275+
return err
276+
}
277+
278+
return nil
279+
}
280+
281+
// runGoModTidy runs 'go mod tidy' in the specified directory.
282+
func runGoModTidy(dir string) error {
283+
cmd := exec.Command("go", "mod", "tidy")
284+
cmd.Dir = dir
285+
286+
// Capture both stdout and stderr
287+
output, err := cmd.CombinedOutput()
288+
if err != nil {
289+
return fmt.Errorf("go mod tidy failed: %w\nOutput: %s", err, string(output))
290+
}
291+
292+
return nil
293+
}

0 commit comments

Comments
 (0)