Skip to content
Draft
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
9 changes: 9 additions & 0 deletions src/cmd/cmd_suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,15 @@ func runCLI(dir string, args ...string) *gexec.Session {
return session
}

func verifyPatternMetadataCreated(dir, patternName string) {
metadataPath := filepath.Join(dir, "pattern-metadata.yaml")
Expect(metadataPath).To(BeAnExistingFile(), "pattern-metadata.yaml should exist")

content, err := os.ReadFile(metadataPath)
Expect(err).NotTo(HaveOccurred())
Expect(string(content)).To(ContainSubstring("name: " + patternName))
}

func verifySkillsInstalled(dir string) {
for _, target := range []string{".claude", ".cursor"} {
skillDir := filepath.Join(dir, target, "skills", "pattern-author")
Expand Down
4 changes: 4 additions & 0 deletions src/cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ func runInit(withSecrets bool) error {
}
}

if err := pattern.GeneratePatternMetadata(actualPatternName, repoRoot); err != nil {
return fmt.Errorf("error generating pattern metadata: %w", err)
}

if err := fileutils.InstallSkills(repoRoot); err != nil {
return fmt.Errorf("error installing skills: %w", err)
}
Expand Down
23 changes: 23 additions & 0 deletions src/cmd/init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@ var _ = Describe("patternizer init", func() {
verifySkillsInstalled(tempDir)
})

It("should create a pattern-metadata.yaml file", func() {
verifyPatternMetadataCreated(tempDir, filepath.Base(tempDir))
})

It("should create an appropriate global values file", func() {
globalValuesFile := filepath.Join(tempDir, "values-global.yaml")
expectedGlobalValues := types.ValuesGlobal{
Expand Down Expand Up @@ -763,6 +767,25 @@ var _ = Describe("patternizer init --with-secrets", func() {
})
})

var _ = Describe("patternizer init pattern-metadata", func() {
Context("on a directory with an existing pattern-metadata.yaml", Ordered, func() {
var tempDir string
const existingContent = "metadata_version: \"1.0\"\nname: custom-name\n"

BeforeAll(func() {
tempDir = createTestDir()
Expect(os.WriteFile(filepath.Join(tempDir, "pattern-metadata.yaml"), []byte(existingContent), 0o644)).To(Succeed())
_ = runCLI(tempDir, "init")
})

It("should not overwrite the existing pattern-metadata.yaml", func() {
actual, err := os.ReadFile(filepath.Join(tempDir, "pattern-metadata.yaml"))
Expect(err).NotTo(HaveOccurred())
Expect(string(actual)).To(Equal(existingContent))
})
})
})

var _ = Describe("patternizer init namespace migration", func() {
Context("on a directory with list-style namespaces", Ordered, func() {
var tempDir string
Expand Down
45 changes: 45 additions & 0 deletions src/internal/embedded/resources/pattern-metadata.yaml.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Schema: https://github.com/validatedpatterns/pattern-ui-catalog/blob/main/pattern.schema.json
metadata_version: "1.0"
name: {{ .Name }}
description: A short description of the pattern
pattern_version: "1.0"
display_name: {{ .DisplayName }}
repo_url: {{ .RepoURL }}
docs_repo_url: https://github.com/validatedpatterns/docs
issues_url: {{ .IssuesURL }}
docs_url: https://validatedpatterns.io/patterns/{{ .Name }}/
ci_url: https://validatedpatterns.io/ci/?pattern={{ .Name }}
tier: sandbox
owners:
- CHANGEME
org: {{ .Org }}
# requirements:
# hub:
# compute:
# aws:
# replicas: 3
# type: m5.2xlarge
# gcp:
# replicas: 3
# type: n1-standard-8
# azure:
# replicas: 3
# type: Standard_D8s_v3
# controlPlane:
# aws:
# replicas: 3
# type: m5.xlarge
# gcp:
# replicas: 3
# type: n1-standard-4
# azure:
# replicas: 3
# type: Standard_D4s_v3
extra_features:
hypershift_support: false
spoke_support: false
# external_requirements:
# variants:
# - name: default
# default: true
# description: Default variant
113 changes: 113 additions & 0 deletions src/internal/pattern/metadata.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package pattern

import (
"fmt"
"net/url"
"os"
"os/exec"
"path/filepath"
"strings"
"text/template"

"github.com/validatedpatterns/patternizer/internal/embedded"
)

type metadataTemplateData struct {
Name string
DisplayName string
RepoURL string
IssuesURL string
Org string
}

// GeneratePatternMetadata creates a pattern-metadata.yaml file in the repo root.
// It skips generation if the file already exists.
func GeneratePatternMetadata(patternName, repoRoot string) error {
metadataPath := filepath.Join(repoRoot, "pattern-metadata.yaml")
if _, err := os.Stat(metadataPath); err == nil {
return nil
}

org, repoURL := detectGitRemote(patternName, repoRoot)

data := metadataTemplateData{
Name: patternName,
DisplayName: toDisplayName(patternName),
RepoURL: repoURL,
IssuesURL: repoURL + "/issues",
Org: org,
}

tmplBytes, err := embedded.Resources.ReadFile("resources/pattern-metadata.yaml.tmpl")
if err != nil {
return fmt.Errorf("reading metadata template: %w", err)
}

tmpl, err := template.New("metadata").Parse(string(tmplBytes))
if err != nil {
return fmt.Errorf("parsing metadata template: %w", err)
}

f, err := os.Create(metadataPath)
if err != nil {
return fmt.Errorf("creating %s: %w", metadataPath, err)
}
defer f.Close()

if err := tmpl.Execute(f, data); err != nil {
return fmt.Errorf("executing metadata template: %w", err)
}

return os.Chmod(metadataPath, 0o644)
}

func detectGitRemote(patternName, repoRoot string) (org, repoURL string) {
fallbackOrg := "CHANGEME"
fallbackURL := "https://github.com/" + fallbackOrg + "/" + patternName

out, err := exec.Command("git", "-C", repoRoot, "remote", "get-url", "origin").Output()
if err != nil {
return fallbackOrg, fallbackURL
}

raw := strings.TrimSpace(string(out))
if raw == "" {
return fallbackOrg, fallbackURL
}

return parseGitRemoteURL(raw, fallbackOrg, fallbackURL)
}

func parseGitRemoteURL(raw, fallbackOrg, fallbackURL string) (org, repoURL string) {
if strings.HasPrefix(raw, "git@") {
raw = strings.TrimPrefix(raw, "git@")
raw = strings.Replace(raw, ":", "/", 1)
raw = "https://" + raw
}

raw = strings.TrimSuffix(raw, ".git")

parsed, err := url.Parse(raw)
if err != nil {
return fallbackOrg, fallbackURL
}

parts := strings.Split(strings.Trim(parsed.Path, "/"), "/")
if len(parts) < 2 {
return fallbackOrg, fallbackURL
}

org = parts[0]
httpsURL := fmt.Sprintf("https://%s/%s/%s", parsed.Host, parts[0], parts[1])
return org, httpsURL
}

func toDisplayName(name string) string {
words := strings.Split(name, "-")
for i, w := range words {
if w != "" {
words[i] = strings.ToUpper(w[:1]) + w[1:]
}
}
return strings.Join(words, " ")
}
134 changes: 134 additions & 0 deletions src/internal/pattern/metadata_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package pattern

import (
"os"
"os/exec"
"path/filepath"
"strings"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)

var _ = Describe("GeneratePatternMetadata", func() {
Context("on a directory without a git remote", func() {
var tempDir string

BeforeEach(func() {
tempDir = GinkgoT().TempDir()
})

It("should create pattern-metadata.yaml with fallback values", func() {
Expect(GeneratePatternMetadata("my-pattern", tempDir)).To(Succeed())

metadataPath := filepath.Join(tempDir, "pattern-metadata.yaml")
Expect(metadataPath).To(BeAnExistingFile())

content, err := os.ReadFile(metadataPath)
Expect(err).NotTo(HaveOccurred())

s := string(content)
Expect(s).To(ContainSubstring("name: my-pattern"))
Expect(s).To(ContainSubstring("display_name: My Pattern"))
Expect(s).To(ContainSubstring("org: CHANGEME"))
Expect(s).To(ContainSubstring("repo_url: https://github.com/CHANGEME/my-pattern"))
Expect(s).To(ContainSubstring("issues_url: https://github.com/CHANGEME/my-pattern/issues"))
Expect(s).To(ContainSubstring("tier: sandbox"))
Expect(s).To(ContainSubstring("# requirements:"))
Expect(s).To(ContainSubstring("extra_features:"))
})
})

Context("on a directory with a git remote", func() {
var tempDir string

BeforeEach(func() {
tempDir = GinkgoT().TempDir()
cmd := exec.Command("git", "init", tempDir)
Expect(cmd.Run()).To(Succeed())
cmd = exec.Command("git", "-C", tempDir, "remote", "add", "origin", "https://github.com/testorg/my-pattern.git")
Expect(cmd.Run()).To(Succeed())
})

It("should detect org and repo URL from git remote", func() {
Expect(GeneratePatternMetadata("my-pattern", tempDir)).To(Succeed())

content, err := os.ReadFile(filepath.Join(tempDir, "pattern-metadata.yaml"))
Expect(err).NotTo(HaveOccurred())

s := string(content)
Expect(s).To(ContainSubstring("org: testorg"))
Expect(s).To(ContainSubstring("repo_url: https://github.com/testorg/my-pattern"))
Expect(s).To(ContainSubstring("issues_url: https://github.com/testorg/my-pattern/issues"))
})
})

Context("when pattern-metadata.yaml already exists", func() {
var tempDir string

BeforeEach(func() {
tempDir = GinkgoT().TempDir()
Expect(os.WriteFile(filepath.Join(tempDir, "pattern-metadata.yaml"), []byte("existing content"), 0o644)).To(Succeed())
})

It("should not overwrite the existing file", func() {
Expect(GeneratePatternMetadata("my-pattern", tempDir)).To(Succeed())

content, err := os.ReadFile(filepath.Join(tempDir, "pattern-metadata.yaml"))
Expect(err).NotTo(HaveOccurred())
Expect(string(content)).To(Equal("existing content"))
})
})
})

var _ = Describe("parseGitRemoteURL", func() {
DescribeTable("should parse various remote URL formats",
func(raw, expectedOrg, expectedURL string) {
org, repoURL := parseGitRemoteURL(raw, "fallback-org", "https://fallback.example.com")
Expect(org).To(Equal(expectedOrg))
Expect(repoURL).To(Equal(expectedURL))
},
Entry("HTTPS URL", "https://github.com/validatedpatterns/multicloud-gitops.git",
"validatedpatterns", "https://github.com/validatedpatterns/multicloud-gitops"),
Entry("HTTPS URL without .git", "https://github.com/validatedpatterns/multicloud-gitops",
"validatedpatterns", "https://github.com/validatedpatterns/multicloud-gitops"),
Entry("SSH URL", "git@github.com:validatedpatterns/multicloud-gitops.git",
"validatedpatterns", "https://github.com/validatedpatterns/multicloud-gitops"),
)

It("should fall back on invalid URLs", func() {
org, repoURL := parseGitRemoteURL("not-a-url", "fallback-org", "https://fallback.example.com")
Expect(org).To(Equal("fallback-org"))
Expect(repoURL).To(Equal("https://fallback.example.com"))
})
})

var _ = Describe("toDisplayName", func() {
DescribeTable("should convert kebab-case to title case",
func(input, expected string) {
Expect(toDisplayName(input)).To(Equal(expected))
},
Entry("single word", "pattern", "Pattern"),
Entry("two words", "multicloud-gitops", "Multicloud Gitops"),
Entry("three words", "my-cool-pattern", "My Cool Pattern"),
)
})

var _ = Describe("detectGitRemote", func() {
It("should return fallback values for a non-git directory", func() {
tempDir := GinkgoT().TempDir()
org, repoURL := detectGitRemote("test-pattern", tempDir)
Expect(org).To(Equal("CHANGEME"))
Expect(repoURL).To(Equal("https://github.com/CHANGEME/test-pattern"))
})

It("should detect remote from a git repo", func() {
tempDir := GinkgoT().TempDir()
Expect(exec.Command("git", "init", tempDir).Run()).To(Succeed())
Expect(exec.Command("git", "-C", tempDir, "remote", "add", "origin", "git@github.com:myorg/myrepo.git").Run()).To(Succeed())

org, repoURL := detectGitRemote("myrepo", tempDir)
Expect(org).To(Equal("myorg"))
Expect(strings.HasPrefix(repoURL, "https://github.com/myorg/myrepo")).To(BeTrue())
})
})
Loading