Skip to content
Open
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
91 changes: 88 additions & 3 deletions internal/registry/github.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ import (
"bytes"
"fmt"
"net/url"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"text/template"

Expand All @@ -18,13 +21,29 @@ type GithubIssue struct {
Body string
}

func CreateGithubIssue(b Buildpack) (GithubIssue, error) {
titleTemplate, err := template.New("buildpack").Parse(GithubIssueTitleTemplate)
// CreateGithubIssue creates a GitHub issue from a buildpack. If registryRoot is provided,
// it will attempt to load both title and body templates from the registry-index repository,
// falling back to hardcoded templates if the file doesn't exist.
// Returns the issue and a boolean indicating whether templates were loaded from the registry.
func CreateGithubIssue(b Buildpack, registryRoot ...string) (GithubIssue, error) {
titleTemplateStr := GithubIssueTitleTemplate
bodyTemplateStr := GithubIssueBodyTemplate

// Try to load templates from registry if root is provided
if len(registryRoot) > 0 && registryRoot[0] != "" {
if title, body, err := loadTemplatesFromRegistry(registryRoot[0], b.Yanked); err == nil {
titleTemplateStr = title
bodyTemplateStr = body
}
// Silently fall back to hardcoded templates if loading fails
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would at least throw a warning message in the log to inform that there was some issue parsing the template, and we are falling back to a hardcoded template

}

titleTemplate, err := template.New("buildpack").Parse(titleTemplateStr)
if err != nil {
return GithubIssue{}, err
}

bodyTemplate, err := template.New("buildpack").Parse(GithubIssueBodyTemplate)
bodyTemplate, err := template.New("buildpack").Parse(bodyTemplateStr)
if err != nil {
return GithubIssue{}, err
}
Expand All @@ -47,6 +66,72 @@ func CreateGithubIssue(b Buildpack) (GithubIssue, error) {
}, nil
}

// loadTemplatesFromRegistry loads both title and body templates from the registry-index repository.
// It parses the YAML frontmatter to extract the title and extracts the body from the markdown content.
func loadTemplatesFromRegistry(registryRoot string, yanked bool) (string, string, error) {
var templatePath string
if yanked {
templatePath = ".github/ISSUE_TEMPLATE/yank-buildpack.md"
} else {
templatePath = ".github/ISSUE_TEMPLATE/add-buildpack.md"
}

fullPath := filepath.Join(registryRoot, templatePath)
content, err := os.ReadFile(fullPath)
if err != nil {
return "", "", err
}

contentStr := string(content)

// Parse YAML frontmatter to extract title
titleTemplate, bodyTemplate, err := parseTemplateFile(contentStr)
if err != nil {
return "", "", err
}

// Convert placeholders to Go template syntax
titleTemplate = convertPlaceholdersToGoTemplate(titleTemplate)
bodyTemplate = convertPlaceholdersToGoTemplate(bodyTemplate)

return titleTemplate, bodyTemplate, nil
}

// parseTemplateFile parses a GitHub issue template file with YAML frontmatter
// and returns the title template and body template.
func parseTemplateFile(content string) (string, string, error) {
// Split frontmatter from body
parts := strings.SplitN(content, "---\n", 3)
if len(parts) < 3 {
return "", "", errors.New("invalid template format: missing YAML frontmatter")
}

frontmatter := parts[1]
body := parts[2]

// Extract title from frontmatter
titleMatch := regexp.MustCompile(`(?m)^title:\s*(.+)$`).FindStringSubmatch(frontmatter)
if len(titleMatch) < 2 {
return "", "", errors.New("invalid template format: missing title in frontmatter")
}
titleTemplate := strings.TrimSpace(titleMatch[1])
// Remove quotes if present
titleTemplate = strings.Trim(titleTemplate, `"'`)

return titleTemplate, strings.TrimSpace(body), nil
}

// convertPlaceholdersToGoTemplate converts template placeholders like {BUILDPACK_ID} and {VERSION}
// to Go template syntax like {{.Namespace}}/{{.Name}} and {{.Version}}.
func convertPlaceholdersToGoTemplate(templateStr string) string {
// Replace {BUILDPACK_ID} with {{.Namespace}}/{{.Name}}
templateStr = strings.ReplaceAll(templateStr, "{BUILDPACK_ID}", "{{.Namespace}}/{{.Name}}")
// Replace {VERSION} with {{.Version}}
templateStr = strings.ReplaceAll(templateStr, "{VERSION}", "{{.Version}}")

return templateStr
}

func CreateBrowserCmd(browserURL, os string) (*exec.Cmd, error) {
_, err := url.ParseRequestURI(browserURL)
if err != nil {
Expand Down
6 changes: 1 addition & 5 deletions internal/registry/registry_cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,7 @@ type Cache struct {
}

const GithubIssueTitleTemplate = "{{ if .Yanked }}YANK{{ else }}ADD{{ end }} {{.Namespace}}/{{.Name}}@{{.Version}}"
const GithubIssueBodyTemplate = `
id = "{{.Namespace}}/{{.Name}}"
version = "{{.Version}}"
{{ if .Yanked }}{{ else if .Address }}addr = "{{.Address}}"{{ end }}
`
const GithubIssueBodyTemplate = "```\nid = \"{{.Namespace}}/{{.Name}}\"\nversion = \"{{.Version}}\"\n{{ if .Yanked }}yank = true\n{{ end }}{{ if .Address }}addr = \"{{.Address}}\"\n{{ end }}```"
const GitCommitTemplate = `{{ if .Yanked }}YANK{{else}}ADD{{end}} {{.Namespace}}/{{.Name}}@{{.Version}}`

// Entry is a list of buildpacks stored in a registry
Expand Down
28 changes: 27 additions & 1 deletion pkg/client/yank_buildpack.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"net/url"
"runtime"

"github.com/buildpacks/pack/internal/config"
"github.com/buildpacks/pack/internal/registry"
)

Expand Down Expand Up @@ -36,14 +37,39 @@ func (c *Client) YankBuildpack(opts YankBuildpackOptions) error {
Yanked: opts.Yank,
}

issue, err := registry.CreateGithubIssue(buildpack)
// Try to get registry cache root to load template from registry-index
var registryRoot string
home, err := config.PackHome()
if err == nil {
registryCache, err := registry.NewRegistryCache(c.logger, home, opts.URL)
if err == nil {
// Initialize the cache if needed (this will clone/refresh the registry)
if err := registryCache.Initialize(); err == nil {
// Refresh to get latest template
_ = registryCache.Refresh()
registryRoot = registryCache.Root
}
} else {
c.logger.Warnf("Error initializing registry cache: %s", err)
}
}

issue, err := registry.CreateGithubIssue(buildpack, registryRoot)
if err != nil {
return err
}

params := url.Values{}
params.Add("title", issue.Title)
params.Add("body", issue.Body)
// Add template parameter when we have a registry root (GitHub will use the template if it exists)
if registryRoot != "" {
if opts.Yank {
params.Add("template", "yank-buildpack.md")
} else {
params.Add("template", "add-buildpack.md")
}
}
issueURL.RawQuery = params.Encode()

c.logger.Debugf("Open URL in browser: %s", issueURL)
Expand Down
Loading