mirror of
https://github.com/ivuorinen/gh-action-readme.git
synced 2026-02-09 05:47:23 +00:00
This commit represents a comprehensive refactoring of the codebase focused on improving code quality, testability, and maintainability. Key improvements: - Implement dependency injection and interface-based architecture - Add comprehensive test framework with fixtures and test suites - Fix all linting issues (errcheck, gosec, staticcheck, goconst, etc.) - Achieve full EditorConfig compliance across all files - Replace hardcoded test data with proper fixture files - Add configuration loader with hierarchical config support - Improve error handling with contextual information - Add progress indicators for better user feedback - Enhance Makefile with help system and improved editorconfig commands - Consolidate constants and remove deprecated code - Strengthen validation logic for GitHub Actions - Add focused consumer interfaces for better separation of concerns Testing improvements: - Add comprehensive integration tests - Implement test executor pattern for better test organization - Create extensive YAML fixture library for testing - Fix all failing tests and improve test coverage - Add validation test fixtures to avoid embedded YAML in Go files Build and tooling: - Update Makefile to show help by default - Fix editorconfig commands to use eclint properly - Add comprehensive help documentation to all make targets - Improve file selection patterns to avoid glob errors This refactoring maintains backward compatibility while significantly improving the internal architecture and developer experience.
259 lines
6.7 KiB
Go
259 lines
6.7 KiB
Go
package internal
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"text/template"
|
|
|
|
"github.com/google/go-github/v57/github"
|
|
|
|
"github.com/ivuorinen/gh-action-readme/internal/cache"
|
|
"github.com/ivuorinen/gh-action-readme/internal/dependencies"
|
|
"github.com/ivuorinen/gh-action-readme/internal/git"
|
|
"github.com/ivuorinen/gh-action-readme/internal/validation"
|
|
)
|
|
|
|
const (
|
|
defaultOrgPlaceholder = "your-org"
|
|
defaultRepoPlaceholder = "your-repo"
|
|
)
|
|
|
|
// TemplateOptions defines options for rendering templates.
|
|
type TemplateOptions struct {
|
|
TemplatePath string
|
|
HeaderPath string
|
|
FooterPath string
|
|
Format string // md or html
|
|
}
|
|
|
|
// TemplateData represents all data available to templates.
|
|
type TemplateData struct {
|
|
// Action Data
|
|
*ActionYML
|
|
|
|
// Git Repository Information
|
|
Git git.RepoInfo `json:"git"`
|
|
|
|
// Configuration
|
|
Config *AppConfig `json:"config"`
|
|
|
|
// Computed Values
|
|
UsesStatement string `json:"uses_statement"`
|
|
|
|
// Dependencies (populated by dependency analysis)
|
|
Dependencies []dependencies.Dependency `json:"dependencies,omitempty"`
|
|
}
|
|
|
|
// templateFuncs returns a map of custom template functions.
|
|
func templateFuncs() template.FuncMap {
|
|
return template.FuncMap{
|
|
"lower": strings.ToLower,
|
|
"upper": strings.ToUpper,
|
|
"replace": strings.ReplaceAll,
|
|
"join": strings.Join,
|
|
"gitOrg": getGitOrg,
|
|
"gitRepo": getGitRepo,
|
|
"gitUsesString": getGitUsesString,
|
|
"actionVersion": getActionVersion,
|
|
}
|
|
}
|
|
|
|
// getGitOrg returns the Git organization from template data.
|
|
func getGitOrg(data any) string {
|
|
if td, ok := data.(*TemplateData); ok {
|
|
if td.Git.Organization != "" {
|
|
return td.Git.Organization
|
|
}
|
|
if td.Config.Organization != "" {
|
|
return td.Config.Organization
|
|
}
|
|
}
|
|
return defaultOrgPlaceholder
|
|
}
|
|
|
|
// getGitRepo returns the Git repository name from template data.
|
|
func getGitRepo(data any) string {
|
|
if td, ok := data.(*TemplateData); ok {
|
|
if td.Git.Repository != "" {
|
|
return td.Git.Repository
|
|
}
|
|
if td.Config.Repository != "" {
|
|
return td.Config.Repository
|
|
}
|
|
}
|
|
return defaultRepoPlaceholder
|
|
}
|
|
|
|
// getGitUsesString returns a complete uses string for the action.
|
|
func getGitUsesString(data any) string {
|
|
td, ok := data.(*TemplateData)
|
|
if !ok {
|
|
return "your-org/your-action@v1"
|
|
}
|
|
|
|
org := strings.TrimSpace(getGitOrg(data))
|
|
repo := strings.TrimSpace(getGitRepo(data))
|
|
|
|
if !isValidOrgRepo(org, repo) {
|
|
return "your-org/your-action@v1"
|
|
}
|
|
|
|
version := formatVersion(getActionVersion(data))
|
|
return buildUsesString(td, org, repo, version)
|
|
}
|
|
|
|
// isValidOrgRepo checks if org and repo are valid.
|
|
func isValidOrgRepo(org, repo string) bool {
|
|
return org != "" && repo != "" && org != defaultOrgPlaceholder && repo != defaultRepoPlaceholder
|
|
}
|
|
|
|
// formatVersion ensures version has proper @ prefix.
|
|
func formatVersion(version string) string {
|
|
version = strings.TrimSpace(version)
|
|
if version == "" {
|
|
return "@v1"
|
|
}
|
|
if !strings.HasPrefix(version, "@") {
|
|
return "@" + version
|
|
}
|
|
return version
|
|
}
|
|
|
|
// buildUsesString constructs the uses string with optional action name.
|
|
func buildUsesString(td *TemplateData, org, repo, version string) string {
|
|
if td.Name != "" {
|
|
actionName := validation.SanitizeActionName(td.Name)
|
|
if actionName != "" && actionName != repo {
|
|
return fmt.Sprintf("%s/%s/%s%s", org, repo, actionName, version)
|
|
}
|
|
}
|
|
return fmt.Sprintf("%s/%s%s", org, repo, version)
|
|
}
|
|
|
|
// getActionVersion returns the action version from template data.
|
|
func getActionVersion(data any) string {
|
|
if td, ok := data.(*TemplateData); ok {
|
|
if td.Config.Version != "" {
|
|
return td.Config.Version
|
|
}
|
|
}
|
|
return "v1"
|
|
}
|
|
|
|
// BuildTemplateData constructs comprehensive template data from action and configuration.
|
|
func BuildTemplateData(action *ActionYML, config *AppConfig, repoRoot, actionPath string) *TemplateData {
|
|
data := &TemplateData{
|
|
ActionYML: action,
|
|
Config: config,
|
|
}
|
|
|
|
// Populate Git information
|
|
if repoRoot != "" {
|
|
if info, err := git.DetectRepository(repoRoot); err == nil {
|
|
data.Git = *info
|
|
}
|
|
}
|
|
|
|
// Override with configuration values if available
|
|
if config.Organization != "" {
|
|
data.Git.Organization = config.Organization
|
|
}
|
|
if config.Repository != "" {
|
|
data.Git.Repository = config.Repository
|
|
}
|
|
|
|
// Build uses statement
|
|
data.UsesStatement = getGitUsesString(data)
|
|
|
|
// Add dependency analysis if enabled
|
|
if config.AnalyzeDependencies && actionPath != "" {
|
|
data.Dependencies = analyzeDependencies(actionPath, config, data.Git)
|
|
}
|
|
|
|
return data
|
|
}
|
|
|
|
// analyzeDependencies performs dependency analysis on the action file.
|
|
func analyzeDependencies(actionPath string, config *AppConfig, gitInfo git.RepoInfo) []dependencies.Dependency {
|
|
// Create GitHub client if we have a token
|
|
var client *GitHubClient
|
|
if token := GetGitHubToken(config); token != "" {
|
|
var err error
|
|
client, err = NewGitHubClient(token)
|
|
if err != nil {
|
|
// Log error but continue with no client (graceful degradation)
|
|
client = nil
|
|
}
|
|
}
|
|
|
|
// Create high-performance cache
|
|
var depCache dependencies.DependencyCache
|
|
if cacheInstance, err := cache.NewCache(cache.DefaultConfig()); err == nil {
|
|
depCache = dependencies.NewCacheAdapter(cacheInstance)
|
|
} else {
|
|
// Fallback to no-op cache if cache creation fails
|
|
depCache = dependencies.NewNoOpCache()
|
|
}
|
|
|
|
// Create dependency analyzer
|
|
var githubClient *github.Client
|
|
if client != nil {
|
|
githubClient = client.Client
|
|
}
|
|
|
|
analyzer := dependencies.NewAnalyzer(githubClient, gitInfo, depCache)
|
|
|
|
// Analyze dependencies
|
|
deps, err := analyzer.AnalyzeActionFile(actionPath)
|
|
if err != nil {
|
|
// Log error but don't fail - return empty dependencies
|
|
return []dependencies.Dependency{}
|
|
}
|
|
|
|
return deps
|
|
}
|
|
|
|
// RenderReadme renders a README using a Go template and the parsed action.yml data.
|
|
func RenderReadme(action any, opts TemplateOptions) (string, error) {
|
|
tmplContent, err := os.ReadFile(opts.TemplatePath)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
var tmpl *template.Template
|
|
if opts.Format == OutputFormatHTML {
|
|
tmpl, err = template.New("readme").Funcs(templateFuncs()).Parse(string(tmplContent))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
var head, foot string
|
|
if opts.HeaderPath != "" {
|
|
h, _ := os.ReadFile(opts.HeaderPath)
|
|
head = string(h)
|
|
}
|
|
if opts.FooterPath != "" {
|
|
f, _ := os.ReadFile(opts.FooterPath)
|
|
foot = string(f)
|
|
}
|
|
// Wrap template output in header/footer
|
|
buf := &bytes.Buffer{}
|
|
buf.WriteString(head)
|
|
if err := tmpl.Execute(buf, action); err != nil {
|
|
return "", err
|
|
}
|
|
buf.WriteString(foot)
|
|
return buf.String(), nil
|
|
}
|
|
|
|
tmpl, err = template.New("readme").Funcs(templateFuncs()).Parse(string(tmplContent))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
buf := &bytes.Buffer{}
|
|
if err := tmpl.Execute(buf, action); err != nil {
|
|
return "", err
|
|
}
|
|
return buf.String(), nil
|
|
}
|