feat: go 1.25.5, dependency updates, renamed internal/errors (#129)

* feat: rename internal/errors to internal/apperrors

* fix(tests): clear env values before using in tests

* feat: rename internal/errors to internal/apperrors

* chore(deps): update go and all dependencies

* chore: remove renovate from pre-commit, formatting

* chore: sonarcloud fixes

* feat: consolidate constants to appconstants/constants.go

* chore: sonarcloud fixes

* feat: simplification, deduplication, test utils

* chore: sonarcloud fixes

* chore: sonarcloud fixes

* chore: sonarcloud fixes

* chore: sonarcloud fixes

* chore: clean up

* fix: config discovery, const deduplication

* chore: fixes
This commit is contained in:
2026-01-01 23:17:29 +02:00
committed by GitHub
parent 85a439d804
commit 7f80105ff5
65 changed files with 2321 additions and 1710 deletions

View File

@@ -12,6 +12,7 @@ import (
"github.com/google/go-github/v74/github"
"github.com/ivuorinen/gh-action-readme/appconstants"
"github.com/ivuorinen/gh-action-readme/internal/git"
)
@@ -27,49 +28,6 @@ const (
BranchName VersionType = "branch"
// LocalPath represents a local file path reference.
LocalPath VersionType = "local"
// Common string constants.
compositeUsing = "composite"
updateTypeNone = "none"
updateTypeMajor = "major"
updateTypePatch = "patch"
updateTypeMinor = "minor"
defaultBranch = "main"
// Timeout constants.
apiCallTimeout = 10 * time.Second
cacheDefaultTTL = 1 * time.Hour
// File permission constants.
backupFilePerms = 0600
updatedFilePerms = 0600
// GitHub URL patterns.
githubBaseURL = "https://github.com"
marketplaceBaseURL = "https://github.com/marketplace/actions/"
// Version parsing constants.
fullSHALength = 40
minSHALength = 7
versionPartsCount = 3
// File path patterns.
dockerPrefix = "docker://"
localPathPrefix = "./"
localPathUpPrefix = "../"
// File extensions.
backupExtension = ".backup"
// Cache key prefixes.
cacheKeyLatest = "latest:"
cacheKeyRepo = "repo:"
// YAML structure constants.
usesFieldPrefix = "uses: "
// Special line estimation for script URLs.
scriptLineEstimate = 10
)
// Dependency represents a GitHub Action dependency with detailed information.
@@ -188,13 +146,16 @@ func (a *Analyzer) CheckOutdated(deps []Dependency) ([]OutdatedDependency, error
}
updateType := a.compareVersions(currentVersion, latestVersion)
if updateType != updateTypeNone {
if updateType != appconstants.UpdateTypeNone {
outdated = append(outdated, OutdatedDependency{
Current: dep,
LatestVersion: latestVersion,
LatestSHA: latestSHA,
UpdateType: updateType,
IsSecurityUpdate: updateType == updateTypeMajor, // Assume major updates might be security
Current: dep,
LatestVersion: latestVersion,
LatestSHA: latestSHA,
UpdateType: updateType,
// Don't assume major version bumps are security updates
// This should only be set if confirmed by security advisory data
// Future enhancement: integrate with GitHub Security Advisories API
IsSecurityUpdate: false,
})
}
}
@@ -252,7 +213,7 @@ func (a *Analyzer) validateAndCheckComposite(
action *ActionWithComposite,
progressCallback func(current, total int, message string),
) ([]Dependency, bool, error) {
if action.Runs.Using != compositeUsing {
if action.Runs.Using != appconstants.ActionTypeComposite {
if err := a.validateActionType(action.Runs.Using); err != nil {
return nil, false, err
}
@@ -336,13 +297,13 @@ func (a *Analyzer) analyzeActionDependency(step CompositeStep, _ int) (*Dependen
// Build dependency
dep := &Dependency{
Name: fmt.Sprintf("%s/%s", owner, repo),
Name: fmt.Sprintf(appconstants.URLPatternGitHubRepo, owner, repo),
Uses: step.Uses,
Version: version,
VersionType: versionType,
IsPinned: versionType == CommitSHA || (versionType == SemanticVersion && a.isVersionPinned(version)),
Author: owner,
SourceURL: fmt.Sprintf("%s/%s/%s", githubBaseURL, owner, repo),
SourceURL: fmt.Sprintf("%s/%s/%s", appconstants.GitHubBaseURL, owner, repo),
IsLocalAction: isLocal,
IsShellScript: false,
WithParams: a.convertWithParams(step.With),
@@ -350,7 +311,7 @@ func (a *Analyzer) analyzeActionDependency(step CompositeStep, _ int) (*Dependen
// Add marketplace URL for public actions
if !isLocal {
dep.MarketplaceURL = marketplaceBaseURL + repo
dep.MarketplaceURL = fmt.Sprintf("%s%s/%s", appconstants.MarketplaceBaseURL, owner, repo)
}
// Fetch additional metadata from GitHub API if available
@@ -375,11 +336,11 @@ func (a *Analyzer) analyzeShellScript(step CompositeStep, stepNumber int) *Depen
// This would ideally link to the specific line in the action.yml file
scriptURL = fmt.Sprintf(
"%s/%s/%s/blob/%s/action.yml#L%d",
githubBaseURL,
appconstants.GitHubBaseURL,
a.RepoInfo.Organization,
a.RepoInfo.Repository,
a.RepoInfo.DefaultBranch,
stepNumber*scriptLineEstimate,
stepNumber*appconstants.ScriptLineEstimate,
) // Rough estimate
}
@@ -408,11 +369,12 @@ func (a *Analyzer) parseUsesStatement(uses string) (owner, repo, version string,
// - ./local-action
// - docker://alpine:3.14
if strings.HasPrefix(uses, localPathPrefix) || strings.HasPrefix(uses, localPathUpPrefix) {
if strings.HasPrefix(uses, appconstants.LocalPathPrefix) ||
strings.HasPrefix(uses, appconstants.LocalPathUpPrefix) {
return "", "", uses, LocalPath
}
if strings.HasPrefix(uses, dockerPrefix) {
if strings.HasPrefix(uses, appconstants.DockerPrefix) {
return "", "", uses, LocalPath
}
@@ -443,9 +405,9 @@ func (a *Analyzer) parseUsesStatement(uses string) (owner, repo, version string,
// isCommitSHA checks if a version string is a commit SHA.
func (a *Analyzer) isCommitSHA(version string) bool {
// Check if it's a 40-character hex string (full SHA) or 7+ character hex (short SHA)
re := regexp.MustCompile(`^[a-f0-9]{7,40}$`)
re := regexp.MustCompile(appconstants.RegexGitSHA)
return len(version) >= minSHALength && re.MatchString(version)
return len(version) >= appconstants.MinSHALength && re.MatchString(version)
}
// isSemanticVersion checks if a version string follows semantic versioning.
@@ -460,7 +422,7 @@ func (a *Analyzer) isSemanticVersion(version string) bool {
func (a *Analyzer) isVersionPinned(version string) bool {
// Consider it pinned if it specifies patch version (v1.2.3) or is a commit SHA
// Also check for full commit SHAs (40 chars)
if len(version) == fullSHALength {
if len(version) == appconstants.FullSHALength {
return true
}
re := regexp.MustCompile(`^v?\d+\.\d+\.\d+`)
@@ -488,11 +450,11 @@ func (a *Analyzer) getLatestVersion(owner, repo string) (version, sha string, er
return "", "", errors.New("GitHub client not available")
}
ctx, cancel := context.WithTimeout(context.Background(), apiCallTimeout)
ctx, cancel := context.WithTimeout(context.Background(), appconstants.APICallTimeout)
defer cancel()
// Check cache first
cacheKey := cacheKeyLatest + fmt.Sprintf("%s/%s", owner, repo)
cacheKey := appconstants.CacheKeyLatest + fmt.Sprintf(appconstants.URLPatternGitHubRepo, owner, repo)
if version, sha, found := a.getCachedVersion(cacheKey); found {
return version, sha, nil
}
@@ -578,7 +540,7 @@ func (a *Analyzer) cacheVersion(cacheKey, version, sha string) {
}
versionInfo := map[string]string{"version": version, "sha": sha}
_ = a.Cache.SetWithTTL(cacheKey, versionInfo, cacheDefaultTTL)
_ = a.Cache.SetWithTTL(cacheKey, versionInfo, appconstants.CacheDefaultTTL)
}
// compareVersions compares two version strings and returns the update type.
@@ -587,12 +549,12 @@ func (a *Analyzer) compareVersions(current, latest string) string {
latestClean := strings.TrimPrefix(latest, "v")
if currentClean == latestClean {
return updateTypeNone
return appconstants.UpdateTypeNone
}
// Special case: floating major version (e.g., "4" -> "4.1.1") should be patch
if !strings.Contains(currentClean, ".") && strings.HasPrefix(latestClean, currentClean+".") {
return updateTypePatch
return appconstants.UpdateTypePatch
}
currentParts := a.parseVersionParts(currentClean)
@@ -605,7 +567,7 @@ func (a *Analyzer) compareVersions(current, latest string) string {
func (a *Analyzer) parseVersionParts(version string) []string {
parts := strings.Split(version, ".")
// For floating versions like "v4", treat as "v4.0.0" for comparison
for len(parts) < versionPartsCount {
for len(parts) < appconstants.VersionPartsCount {
parts = append(parts, "0")
}
@@ -615,16 +577,16 @@ func (a *Analyzer) parseVersionParts(version string) []string {
// determineUpdateType compares version parts and returns update type.
func (a *Analyzer) determineUpdateType(currentParts, latestParts []string) string {
if currentParts[0] != latestParts[0] {
return updateTypeMajor
return appconstants.UpdateTypeMajor
}
if currentParts[1] != latestParts[1] {
return updateTypeMinor
return appconstants.UpdateTypeMinor
}
if currentParts[2] != latestParts[2] {
return updateTypePatch
return appconstants.UpdateTypePatch
}
return updateTypeNone
return appconstants.UpdateTypeNone
}
// updateActionFile applies updates to a single action file.
@@ -636,8 +598,8 @@ func (a *Analyzer) updateActionFile(filePath string, updates []PinnedUpdate) err
}
// Create backup
backupPath := filePath + backupExtension
if err := os.WriteFile(backupPath, content, backupFilePerms); err != nil { // #nosec G306 -- backup file permissions
backupPath := filePath + appconstants.BackupExtension
if err := os.WriteFile(backupPath, content, appconstants.FilePermDefault); err != nil { // #nosec G306
return fmt.Errorf("failed to create backup: %w", err)
}
@@ -649,7 +611,7 @@ func (a *Analyzer) updateActionFile(filePath string, updates []PinnedUpdate) err
if strings.Contains(line, update.OldUses) {
// Replace the uses statement while preserving indentation
indent := strings.Repeat(" ", len(line)-len(strings.TrimLeft(line, " ")))
lines[i] = indent + usesFieldPrefix + update.NewUses
lines[i] = indent + appconstants.UsesFieldPrefix + update.NewUses
update.LineNumber = i + 1 // Store line number for reference
break
@@ -659,8 +621,7 @@ func (a *Analyzer) updateActionFile(filePath string, updates []PinnedUpdate) err
// Write updated content
updatedContent := strings.Join(lines, "\n")
if err := os.WriteFile(filePath, []byte(updatedContent), updatedFilePerms); err != nil {
// #nosec G306 -- updated file permissions
if err := os.WriteFile(filePath, []byte(updatedContent), appconstants.FilePermDefault); err != nil { // #nosec G306
return fmt.Errorf("failed to write updated file: %w", err)
}
@@ -689,11 +650,11 @@ func (a *Analyzer) validateActionFile(filePath string) error {
// enrichWithGitHubData fetches additional information from GitHub API.
func (a *Analyzer) enrichWithGitHubData(dep *Dependency, owner, repo string) error {
ctx, cancel := context.WithTimeout(context.Background(), apiCallTimeout)
ctx, cancel := context.WithTimeout(context.Background(), appconstants.APICallTimeout)
defer cancel()
// Check cache first
cacheKey := cacheKeyRepo + fmt.Sprintf("%s/%s", owner, repo)
cacheKey := appconstants.CacheKeyRepo + fmt.Sprintf("%s/%s", owner, repo)
if a.Cache != nil {
if cached, exists := a.Cache.Get(cacheKey); exists {
if repository, ok := cached.(*github.Repository); ok {
@@ -712,7 +673,7 @@ func (a *Analyzer) enrichWithGitHubData(dep *Dependency, owner, repo string) err
// Cache the result with 1 hour TTL
if a.Cache != nil {
_ = a.Cache.SetWithTTL(cacheKey, repository, cacheDefaultTTL) // Ignore cache errors
_ = a.Cache.SetWithTTL(cacheKey, repository, appconstants.CacheDefaultTTL) // Ignore cache errors
}
// Enrich dependency with API data

View File

@@ -10,6 +10,7 @@ import (
"github.com/google/go-github/v74/github"
"github.com/ivuorinen/gh-action-readme/appconstants"
"github.com/ivuorinen/gh-action-readme/internal/cache"
"github.com/ivuorinen/gh-action-readme/internal/git"
"github.com/ivuorinen/gh-action-readme/testutil"
@@ -28,14 +29,14 @@ func TestAnalyzer_AnalyzeActionFile(t *testing.T) {
}{
{
name: "simple action - no dependencies",
actionYML: testutil.MustReadFixture("actions/javascript/simple.yml"),
actionYML: testutil.MustReadFixture(appconstants.TestFixtureJavaScriptSimple),
expectError: false,
expectDeps: false,
expectedLen: 0,
},
{
name: "composite action with dependencies",
actionYML: testutil.MustReadFixture("actions/composite/with-dependencies.yml"),
actionYML: testutil.MustReadFixture(appconstants.TestFixtureCompositeWithDeps),
expectError: false,
expectDeps: true,
expectedLen: 5, // 3 action dependencies + 2 shell script dependencies
@@ -43,14 +44,14 @@ func TestAnalyzer_AnalyzeActionFile(t *testing.T) {
},
{
name: "docker action - no step dependencies",
actionYML: testutil.MustReadFixture("actions/docker/basic.yml"),
actionYML: testutil.MustReadFixture(appconstants.TestFixtureDockerBasic),
expectError: false,
expectDeps: false,
expectedLen: 0,
},
{
name: "invalid action file",
actionYML: testutil.MustReadFixture("actions/invalid/invalid-using.yml"),
actionYML: testutil.MustReadFixture(appconstants.TestFixtureInvalidInvalidUsing),
expectError: true,
},
{
@@ -70,7 +71,7 @@ func TestAnalyzer_AnalyzeActionFile(t *testing.T) {
tmpDir, cleanup := testutil.TempDir(t)
defer cleanup()
actionPath := filepath.Join(tmpDir, "action.yml")
actionPath := filepath.Join(tmpDir, appconstants.TestPathActionYML)
testutil.WriteTestFile(t, actionPath, tt.actionYML)
// Create analyzer with mock GitHub client
@@ -429,9 +430,9 @@ func TestAnalyzer_GeneratePinnedUpdate(t *testing.T) {
defer cleanup()
// Create a test action file with composite steps
actionContent := testutil.MustReadFixture("test-composite-action.yml")
actionContent := testutil.MustReadFixture(appconstants.TestFixtureTestCompositeAction)
actionPath := filepath.Join(tmpDir, "action.yml")
actionPath := filepath.Join(tmpDir, appconstants.TestPathActionYML)
testutil.WriteTestFile(t, actionPath, actionContent)
// Create analyzer
@@ -550,8 +551,8 @@ func TestAnalyzer_WithoutGitHubClient(t *testing.T) {
tmpDir, cleanup := testutil.TempDir(t)
defer cleanup()
actionPath := filepath.Join(tmpDir, "action.yml")
testutil.WriteTestFile(t, actionPath, testutil.MustReadFixture("actions/composite/basic.yml"))
actionPath := filepath.Join(tmpDir, appconstants.TestPathActionYML)
testutil.WriteTestFile(t, actionPath, testutil.MustReadFixture(appconstants.TestFixtureCompositeBasic))
deps, err := analyzer.AnalyzeActionFile(actionPath)
@@ -586,7 +587,7 @@ func TestNewAnalyzer(t *testing.T) {
githubClient := testutil.MockGitHubClient(mockResponses)
cacheInstance, err := cache.NewCache(cache.DefaultConfig())
testutil.AssertNoError(t, err)
defer func() { _ = cacheInstance.Close() }()
defer testutil.CleanupCache(t, cacheInstance)()
repoInfo := git.RepoInfo{
Organization: "test-owner",

View File

@@ -5,6 +5,8 @@ import (
"os"
"github.com/goccy/go-yaml"
"github.com/ivuorinen/gh-action-readme/appconstants"
)
// parseCompositeActionFromFile reads and parses a composite action file.
@@ -33,7 +35,7 @@ func (a *Analyzer) parseCompositeAction(actionPath string) (*ActionWithComposite
}
// If this is not a composite action, return empty steps
if action.Runs.Using != compositeUsing {
if action.Runs.Using != appconstants.ActionTypeComposite {
action.Runs.Steps = []CompositeStep{}
}
@@ -47,5 +49,5 @@ func IsCompositeAction(actionPath string) (bool, error) {
return false, err
}
return action.Runs.Using == compositeUsing, nil
return action.Runs.Using == appconstants.ActionTypeComposite, nil
}