Skip to content

Commit 9cbcf5c

Browse files
authored
Update lekko init to install dependencies and add GitHub workflow (#415)
* Add Go deps install and GH workflow * Add confirmation for workflow and some progress messages * Add install deps and workflow emit for TS projects * Add success indicators * Use spinner for progress messages
1 parent 2bbdfc3 commit 9cbcf5c

File tree

3 files changed

+288
-14
lines changed

3 files changed

+288
-14
lines changed

cmd/lekko/init.go

Lines changed: 283 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,32 +18,58 @@ import (
1818
"context"
1919
"fmt"
2020
"os"
21+
"os/exec"
22+
"path/filepath"
2123
"strings"
24+
"time"
2225

2326
"github.com/AlecAivazis/survey/v2"
27+
"github.com/briandowns/spinner"
2428
"github.com/go-git/go-git/v5"
2529
"github.com/lainio/err2"
2630
"github.com/lainio/err2/try"
2731
"github.com/lekkodev/cli/pkg/dotlekko"
2832
"github.com/lekkodev/cli/pkg/gen"
33+
"github.com/lekkodev/cli/pkg/logging"
2934
"github.com/lekkodev/cli/pkg/native"
3035
"github.com/lekkodev/cli/pkg/repo"
3136
"github.com/pkg/errors"
3237
"github.com/spf13/cobra"
3338
)
3439

40+
type projectFramework int
41+
42+
const (
43+
pfUnknown projectFramework = iota
44+
pfGo
45+
pfNode
46+
pfReact
47+
pfVite
48+
pfNext
49+
)
50+
51+
type packageManager string
52+
53+
const (
54+
pmUnknown packageManager = ""
55+
pmNPM packageManager = "npm"
56+
pmYarn packageManager = "yarn"
57+
)
58+
3559
func initCmd() *cobra.Command {
3660
var lekkoPath, repoName string
3761
cmd := &cobra.Command{
3862
Use: "init",
3963
Short: "initialize Lekko in your project",
4064
RunE: func(cmd *cobra.Command, args []string) (err error) {
4165
defer err2.Handle(&err)
66+
successCheck := logging.Green("\u2713")
67+
spin := spinner.New(spinner.CharSets[14], 100*time.Millisecond)
4268
// TODO:
4369
// + create .lekko file
4470
// + generate from `default` namespace
45-
// - install lekko deps (depending on project type)
46-
// - setup github actions
71+
// + install lekko deps (depending on project type)
72+
// + setup github actions
4773
// - install linter
4874
_, err = dotlekko.ReadDotLekko("")
4975
if err == nil {
@@ -53,23 +79,45 @@ func initCmd() *cobra.Command {
5379
// TODO: print some info
5480

5581
// naive check for "known" project types
56-
isGo := false
57-
isNode := false
82+
// TODO: Consolidate into DetectNativeLang
83+
pf := pfUnknown
84+
pm := pmUnknown
5885
if _, err = os.Stat("go.mod"); err == nil {
59-
isGo = true
86+
pf = pfGo
6087
} else if _, err = os.Stat("package.json"); err == nil {
61-
isNode = true
88+
pf = pfNode
89+
pjBytes, err := os.ReadFile("package.json")
90+
if err != nil {
91+
return errors.Wrap(err, "failed to open package.json")
92+
}
93+
pjString := string(pjBytes)
94+
if strings.Contains(pjString, "react-dom") {
95+
pf = pfReact
96+
}
97+
// Vite config file could be js, cjs, mjs, etc.
98+
if matches, err := filepath.Glob("vite.config.*"); matches != nil && err == nil {
99+
pf = pfVite
100+
}
101+
// Next config file could be js, cjs, mjs, etc.
102+
if matches, err := filepath.Glob("next.config.*"); matches != nil && err == nil {
103+
pf = pfNext
104+
}
105+
106+
pm = pmNPM
107+
if _, err := os.Stat("yarn.lock"); err == nil {
108+
pm = pmYarn
109+
}
62110
}
63-
if !isGo && !isNode {
111+
if pf == pfUnknown {
64112
return errors.New("Unknown project type, Lekko currently supports Go and NPM projects.")
65113
}
66114

67115
if lekkoPath == "" {
68116
lekkoPath = "lekko"
69-
if fi, err := os.Stat("src"); err == nil && fi.IsDir() && isNode {
117+
if fi, err := os.Stat("src"); err == nil && fi.IsDir() {
70118
lekkoPath = "src/lekko"
71119
}
72-
if fi, err := os.Stat("internal"); err == nil && fi.IsDir() && isGo {
120+
if fi, err := os.Stat("internal"); err == nil && fi.IsDir() && pf == pfGo {
73121
lekkoPath = "internal/lekko"
74122
}
75123
try.To(survey.AskOne(&survey.Input{
@@ -109,16 +157,169 @@ func initCmd() *cobra.Command {
109157
repoName = fmt.Sprintf("%s/lekko-configs", owner)
110158
}
111159
try.To(survey.AskOne(&survey.Input{
112-
Message: "Config repository name, for example `my-org/lekko-configs`:",
160+
Message: "Lekko repository name, for example `my-org/lekko-configs`:",
113161
Default: repoName,
162+
Help: "If you set up your team on app.lekko.com, you can find your Lekko repository by logging in.",
114163
}, &repoName))
115164
}
116165

117166
dot := dotlekko.NewDotLekko(lekkoPath, repoName)
118167
try.To(dot.WriteBack())
168+
169+
// Add GitHub workflow file
170+
var addWorkflow bool
171+
if err := survey.AskOne(&survey.Confirm{
172+
Message: "Add GitHub workflow file at .github/workflows/lekko.yaml?",
173+
Default: true,
174+
Help: "This workflow will use the Lekko Push Action, which enables the automatic mirrorring feature.",
175+
}, &addWorkflow); err != nil {
176+
return err
177+
}
178+
if addWorkflow {
179+
if err := os.MkdirAll(".github/workflows", os.ModePerm); err != nil {
180+
return errors.Wrap(err, "failed to mkdir .github/workflows")
181+
}
182+
workflowTemplate := getGitHubWorkflowTemplateBase()
183+
if suffix, err := getGitHubWorkflowTemplateSuffix(pf, pm); err != nil {
184+
return err
185+
} else {
186+
workflowTemplate += suffix
187+
}
188+
if err := os.WriteFile(".github/workflows/lekko.yaml", []byte(workflowTemplate), 0600); err != nil {
189+
return errors.Wrap(err, "failed to write Lekko workflow file")
190+
}
191+
// TODO: Consider moving instructions to end?
192+
fmt.Printf("%s Successfully added .github/workflows/lekko.yaml, please make sure to add LEKKO_API_KEY as a secret in your GitHub repository/org settings.\n", successCheck)
193+
}
194+
195+
// TODO: Install deps depending on project type
196+
// TODO: Determine package manager (npm/yarn/pnpm/etc.) for ts projects
197+
spin.Suffix = " Installing dependencies..."
198+
spin.Start()
199+
switch pf {
200+
case pfGo:
201+
{
202+
goGetCmd := exec.Command("go", "get", "github.com/lekkodev/go-sdk@latest")
203+
if out, err := goGetCmd.CombinedOutput(); err != nil {
204+
spin.Stop()
205+
fmt.Println(goGetCmd.String())
206+
fmt.Println(string(out))
207+
return errors.Wrap(err, "failed to run go get")
208+
}
209+
spin.Stop()
210+
fmt.Printf("%s Successfully installed Lekko Go SDK.\n", successCheck)
211+
spin.Start()
212+
}
213+
case pfVite:
214+
// NOTE: Vite doesn't necessarily mean React but we assume for now
215+
{
216+
var installArgs, installDevArgs []string
217+
switch pm {
218+
case pmNPM:
219+
{
220+
installArgs = []string{"install", "@lekko/react-sdk"}
221+
installDevArgs = []string{"install", "-D", "@lekko/vite-plugin", "@lekko/eslint-plugin"}
222+
}
223+
case pmYarn:
224+
{
225+
installArgs = []string{"add", "@lekko/react-sdk"}
226+
installDevArgs = []string{"add", "-D", "@lekko/vite-plugin", "@lekko/eslint-plugin"}
227+
}
228+
default:
229+
{
230+
return errors.Errorf("unsupported package manager %s", pm)
231+
}
232+
}
233+
installCmd := exec.Command(string(pm), installArgs...) // #nosec G204
234+
if out, err := installCmd.CombinedOutput(); err != nil {
235+
spin.Stop()
236+
fmt.Println(installCmd.String())
237+
fmt.Println(string(out))
238+
return errors.Wrap(err, "failed to run install deps command")
239+
}
240+
spin.Stop()
241+
fmt.Printf("%s Successfully installed @lekko/react-sdk.\n", successCheck)
242+
spin.Start()
243+
installCmd = exec.Command(string(pm), installDevArgs...) // #nosec G204
244+
if out, err := installCmd.CombinedOutput(); err != nil {
245+
spin.Stop()
246+
fmt.Println(installCmd.String())
247+
fmt.Println(string(out))
248+
return errors.Wrap(err, "failed to run install dev deps command")
249+
}
250+
spin.Stop()
251+
fmt.Printf("%s Successfully installed @lekko/vite-plugin and @lekko/eslint-plugin. See the docs to configure these plugins.\n", successCheck)
252+
spin.Start()
253+
}
254+
case pfNext:
255+
{
256+
var installArgs, installDevArgs []string
257+
switch pm {
258+
case pmNPM:
259+
{
260+
installArgs = []string{"install", "@lekko/next-sdk"}
261+
installDevArgs = []string{"install", "-D", "@lekko/eslint-plugin"}
262+
}
263+
case pmYarn:
264+
{
265+
installArgs = []string{"add", "@lekko/next-sdk"}
266+
installDevArgs = []string{"add", "-D", "@lekko/eslint-plugin"}
267+
}
268+
default:
269+
{
270+
return errors.Errorf("unsupported package manager %s", pm)
271+
}
272+
}
273+
installCmd := exec.Command(string(pm), installArgs...) // #nosec G204
274+
if out, err := installCmd.CombinedOutput(); err != nil {
275+
spin.Stop()
276+
fmt.Println(installCmd.String())
277+
fmt.Println(string(out))
278+
return errors.Wrap(err, "failed to run install deps command")
279+
}
280+
spin.Stop()
281+
fmt.Printf("%s Successfully installed @lekko/next-sdk. See the docs to configure the SDK.\n", successCheck)
282+
spin.Start()
283+
installCmd = exec.Command(string(pm), installDevArgs...) // #nosec G204
284+
if out, err := installCmd.CombinedOutput(); err != nil {
285+
spin.Stop()
286+
fmt.Println(installCmd.String())
287+
fmt.Println(string(out))
288+
return errors.Wrap(err, "failed to run install dev deps command")
289+
}
290+
spin.Stop()
291+
fmt.Printf("%s Successfully installed @lekko/eslint-plugin. See the docs to configure this plugin.\n", successCheck)
292+
spin.Start()
293+
}
294+
}
295+
spin.Stop()
296+
297+
// Codegen
298+
spin.Suffix = " Running codegen..."
299+
spin.Start()
119300
// TODO: make sure that `default` namespace exists
120301
try.To(runGen(cmd.Context(), lekkoPath, "default"))
302+
spin.Stop()
121303

304+
// Post-gen steps
305+
spin.Suffix = " Running post-codegen steps..."
306+
spin.Start()
307+
switch pf {
308+
case pfGo:
309+
{
310+
// For Go we want to run `go mod tidy` - this handles transitive deps
311+
goTidyCmd := exec.Command("go", "mod", "tidy")
312+
if out, err := goTidyCmd.CombinedOutput(); err != nil {
313+
spin.Stop()
314+
fmt.Println(goTidyCmd.String())
315+
fmt.Println(string(out))
316+
return errors.Wrap(err, "failed to run go mod tidy")
317+
}
318+
}
319+
}
320+
spin.Stop()
321+
322+
fmt.Printf("%s Complete! Your project is now set up to use Lekko.\n", successCheck)
122323
return nil
123324
},
124325
}
@@ -136,3 +337,75 @@ func runGen(ctx context.Context, lekkoPath, ns string) (err error) {
136337
Namespaces: []string{ns},
137338
})
138339
}
340+
341+
func getGitHubWorkflowTemplateBase() string {
342+
// TODO: determine default branch name (might not be main)
343+
return `name: lekko
344+
on:
345+
pull_request:
346+
branches:
347+
- main
348+
push:
349+
branches:
350+
- main
351+
permissions:
352+
contents: read
353+
jobs:
354+
build:
355+
runs-on: ubuntu-latest
356+
steps:
357+
- uses: actions/checkout@v4
358+
`
359+
}
360+
361+
func getGitHubWorkflowTemplateSuffix(pf projectFramework, pm packageManager) (string, error) {
362+
// NOTE: Make sure to keep the indentation matched with base
363+
var ret string
364+
switch pf {
365+
case pfGo:
366+
{
367+
ret = ` - uses: actions/setup-go@v5
368+
with:
369+
go-version-file: go.mod
370+
`
371+
}
372+
case pfNode:
373+
fallthrough
374+
case pfReact:
375+
fallthrough
376+
case pfVite:
377+
fallthrough
378+
case pfNext:
379+
{
380+
ret = ` - uses: actions/setup-node@v4
381+
with:
382+
node-version: lts/Hydrogen
383+
`
384+
switch pm {
385+
case pmNPM:
386+
{
387+
ret += ` - run: npm install
388+
`
389+
}
390+
case pmYarn:
391+
{
392+
ret += ` cache: yarn
393+
- run: yarn install
394+
`
395+
}
396+
default:
397+
return "", errors.New("unsupported package manager for GitHub workflow setup")
398+
}
399+
}
400+
// TODO: For TS projects need to detect package manager
401+
default:
402+
{
403+
return "", errors.New("unsupported framework for GitHub workflow setup")
404+
}
405+
}
406+
ret += ` - uses: lekkodev/push-action@v1
407+
with:
408+
api_key: ${{ secrets.LEKKO_API_KEY }}
409+
`
410+
return ret, nil
411+
}

pkg/gitcli/gitcli.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@ package gitcli
1616

1717
import "os/exec"
1818

19-
func Clone(url, path string) error {
19+
func Clone(url, path string) ([]byte, error) {
2020
cmd := exec.Command("git", "clone", url, path)
21-
return cmd.Run()
21+
return cmd.CombinedOutput()
2222
}
2323

2424
func Pull(path string) error {

pkg/repo/cmd.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -161,9 +161,10 @@ func PrepareGithubRepo() (string, error) {
161161
githubRepoURL := fmt.Sprintf("https://github.com/%s/%s.git", repoOwner, repoName)
162162

163163
if shouldClone {
164-
err := gitcli.Clone(githubRepoURL, repoPath)
164+
cloneOut, err := gitcli.Clone(githubRepoURL, repoPath)
165165
if err != nil {
166-
return "", err
166+
fmt.Println(string(cloneOut))
167+
return "", errors.Wrapf(err, "git clone %s to %s", githubRepoURL, repoPath)
167168
}
168169
} else {
169170
gitRepo, err := git.PlainOpen(repoPath)

0 commit comments

Comments
 (0)