Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions internal/check/env.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package check

import (
"bufio"
"context"
"fmt"
"os"
"strings"
)

type EnvCheck struct {
Dir string
}

func (c *EnvCheck) Name() string {
return ".env has all required keys"
}

func (c *EnvCheck) Run(_ context.Context) Result {
exampleKeys, err := parseEnvKeys(c.Dir + "/.env.example")
if err != nil {
return Result{
Name: c.Name(),
Status: StatusFail,
Message: "could not read .env.example",
}
}

actualKeys, err := parseEnvKeys(c.Dir + "/.env")
if err != nil {
return Result{
Name: c.Name(),
Status: StatusFail,
Message: ".env file not found",
Fix: "copy .env.example to .env and fill in the values",
}
}

var missing []string
for key := range exampleKeys {
if _, ok := actualKeys[key]; !ok {
missing = append(missing, key)
}
}

if len(missing) > 0 {
return Result{
Name: c.Name(),
Status: StatusFail,
Message: fmt.Sprintf("missing keys: %s", strings.Join(missing, ", ")),
Fix: "add the missing keys to your .env file",
}
}

return Result{
Name: c.Name(),
Status: StatusPass,
Message: fmt.Sprintf("all %d keys present", len(exampleKeys)),
}
}

func parseEnvKeys(path string) (map[string]struct{}, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()

keys := make(map[string]struct{})
scanner := bufio.NewScanner(f)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || strings.HasPrefix(line, "#") {
continue
}
key, _, _ := strings.Cut(line, "=")
key = strings.TrimSpace(key)
if key != "" {
keys[key] = struct{}{}
}
}
return keys, scanner.Err()
}
54 changes: 54 additions & 0 deletions internal/check/env_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package check

import (
"context"
"os"
"testing"
)

func TestEnvCheck_Pass(t *testing.T) {
dir := t.TempDir()
os.WriteFile(dir+"/.env.example", []byte("DB_URL=\nAPI_KEY=\n"), 0644)
os.WriteFile(dir+"/.env", []byte("DB_URL=postgres://localhost\nAPI_KEY=secret\n"), 0644)

c := &EnvCheck{Dir: dir}
result := c.Run(context.Background())
if result.Status != StatusPass {
t.Errorf("expected pass, got %v: %s", result.Status, result.Message)
}
}

func TestEnvCheck_MissingKeys(t *testing.T) {
dir := t.TempDir()
os.WriteFile(dir+"/.env.example", []byte("DB_URL=\nAPI_KEY=\nSECRET=\n"), 0644)
os.WriteFile(dir+"/.env", []byte("DB_URL=postgres://localhost\n"), 0644)

c := &EnvCheck{Dir: dir}
result := c.Run(context.Background())
if result.Status != StatusFail {
t.Errorf("expected fail, got %v", result.Status)
}
}

func TestEnvCheck_MissingEnvFile(t *testing.T) {
dir := t.TempDir()
os.WriteFile(dir+"/.env.example", []byte("DB_URL=\n"), 0644)

c := &EnvCheck{Dir: dir}
result := c.Run(context.Background())
if result.Status != StatusFail {
t.Errorf("expected fail, got %v", result.Status)
}
}

func TestEnvCheck_IgnoresComments(t *testing.T) {
dir := t.TempDir()
os.WriteFile(dir+"/.env.example", []byte("# this is a comment\nDB_URL=\n\nAPI_KEY=\n"), 0644)
os.WriteFile(dir+"/.env", []byte("DB_URL=postgres://localhost\nAPI_KEY=secret\n"), 0644)

c := &EnvCheck{Dir: dir}
result := c.Run(context.Background())
if result.Status != StatusPass {
t.Errorf("expected pass, got %v: %s", result.Status, result.Message)
}
}
5 changes: 3 additions & 2 deletions internal/check/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,9 @@ func Build(stack detector.DetectedStack) []Check {
// add Redis checks
}

// always run env check if .env.example exists
// cs = append(cs, &EnvCheck{})
if stack.EnvExample {
cs = append(cs, &EnvCheck{Dir: "."})
}

return cs
}
4 changes: 3 additions & 1 deletion internal/detector/detector.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ type DetectedStack struct {
Postgres bool
Redis bool
MySQL bool
MongoDB bool
MongoDB bool
EnvExample bool
}

func Detect(dir string) DetectedStack {
Expand All @@ -39,6 +40,7 @@ func Detect(dir string) DetectedStack {
stack.MySQL = strings.Contains(dbURL, "mysql")
stack.MongoDB = os.Getenv("MONGODB_URI") != "" || os.Getenv("MONGO_URL") != ""
stack.Redis = os.Getenv("REDIS_URL") != "" || os.Getenv("REDIS_URI") != ""
stack.EnvExample = fileExists(filepath.Join(dir, ".env.example"))

return stack
}
Expand Down