feat(conf): support prefix scope matching and release config fallback #102

Open
张泊明518370910136 wants to merge 2 commits from feat/prefix-scope-matching-and-fallback into master
4 changed files with 194 additions and 50 deletions

View File

@ -152,50 +152,115 @@ func hintValidScopes(confRoot, confName string) {
"valid scopes", validScopes)
}
func GetConfPath(confRoot, confName, fallbackConfName, msg, tag string) (
confPath string, confStat fs.FileInfo,
conventionalCommit *ConventionalCommit, err error,
) {
confPath, conventionalCommit, err = parseMsg(confRoot, confName, msg, tag)
if err != nil {
slog.Error("parse msg", "error", err)
// no fallback when tag is specified, it is triggered by release.yaml
if os.IsNotExist(err) {
slog.Info("tag is not empty, no fallback conf")
hintValidScopes(confRoot, confName)
return confPath, confStat, conventionalCommit, err
}
// fallback to conf file in conf root on parse error
confPath = filepath.Join(confRoot, fallbackConfName)
slog.Info("fallback to conf", "path", confPath)
func findBestMatchingScope(confRoot, targetScope, confName string) string {
if targetScope == "" {
return ""
}
confStat, err = os.Stat(confPath)
if err != nil {
if os.IsNotExist(err) {
hintValidScopes(confRoot, confName)
}
slog.Error("stat conf", "error", err)
// fallback to conf file in conf root on conf not exist
confPath = filepath.Join(confRoot, fallbackConfName)
slog.Info("fallback to conf", "path", confPath)
confStat, err = os.Stat(confPath)
confRoot = filepath.Clean(confRoot)
bestMatch := ""
_ = filepath.WalkDir(confRoot, func(path string, d fs.DirEntry, err error) error {
if err != nil {
slog.Error("stat fallback conf", "error", err)
return confPath, confStat, conventionalCommit, err
return nil
}
}
// Check file ownership
if d.IsDir() {
relPath, err := filepath.Rel(confRoot, path)
if err != nil || relPath == "." || relPath == "" || strings.HasPrefix(relPath, "..") {
return nil
}
confPath := filepath.Join(path, confName)
if stat, err := os.Stat(confPath); err == nil && !stat.IsDir() {
if strings.HasPrefix(targetScope, relPath) {
if len(relPath) > len(bestMatch) {
bestMatch = relPath
}
}
}
}
return nil
})
return bestMatch
}
func checkFileOwnership(confPath string, confStat fs.FileInfo) error {
if stat, ok := confStat.Sys().(*syscall.Stat_t); ok {
uid := int(stat.Uid)
currentUID := os.Getuid()
if uid != currentUID {
err = fmt.Errorf("insecure configuration file: owned by uid %d, expected %d", uid, currentUID)
slog.Error("insecure conf file", "path", confPath, "uid", uid, "currentUID", currentUID)
return confPath, confStat, conventionalCommit, err
return fmt.Errorf("insecure configuration file: owned by uid %d, expected %d", uid, currentUID)
}
}
return nil
}
func GetConfPath(confRoot, confName, fallbackConfName, msg, tag string) (
confPath string, confStat fs.FileInfo,
conventionalCommit *ConventionalCommit, err error,
) {
if fallbackConfName == "" {
fallbackConfName = confName
}
confPath, conventionalCommit, err = parseMsg(confRoot, confName, msg, tag)
if err == nil && conventionalCommit != nil && conventionalCommit.Scope != "" {
confStat, err = os.Stat(confPath)
if err == nil && !confStat.IsDir() {
if err := checkFileOwnership(confPath, confStat); err != nil {
return confPath, confStat, conventionalCommit, err
}
return confPath, confStat, conventionalCommit, nil
}
}
return confPath, confStat, conventionalCommit, err
targetScope := ""
if tag != "" {
targetScope = tag
} else if conventionalCommit != nil {
targetScope = conventionalCommit.Scope
}
if targetScope != "" {
if matchedScope := findBestMatchingScope(confRoot, targetScope, confName); matchedScope != "" {
matchedPath := filepath.Join(confRoot, matchedScope, confName)
if stat, err := os.Stat(matchedPath); err == nil && !stat.IsDir() {
slog.Info("matched scope by prefix", "targetScope", targetScope, "matchedScope", matchedScope)
if conventionalCommit == nil {
conventionalCommit = &ConventionalCommit{
Scope: matchedScope,
Group: "all",
}
} else {
conventionalCommit.Scope = matchedScope
}
if err := checkFileOwnership(matchedPath, stat); err != nil {
return matchedPath, stat, conventionalCommit, err
}
return matchedPath, stat, conventionalCommit, nil
}
}
}
fallbackPath := filepath.Join(confRoot, fallbackConfName)
slog.Info("fallback to conf", "path", fallbackPath)
confStat, err = os.Stat(fallbackPath)
if err != nil || confStat.IsDir() {
if os.IsNotExist(err) || (confStat != nil && confStat.IsDir()) {
hintValidScopes(confRoot, confName)
}
slog.Error("stat fallback conf", "error", err)
if confStat != nil && confStat.IsDir() {
err = fmt.Errorf("fallback conf path is a directory: %s", fallbackPath)
}
return fallbackPath, confStat, conventionalCommit, err
}
if err := checkFileOwnership(fallbackPath, confStat); err != nil {
return fallbackPath, confStat, conventionalCommit, err
}
return fallbackPath, confStat, conventionalCommit, nil
}
func MatchGroups(conf *Conf, conventionalCommit *ConventionalCommit) []string {

View File

@ -1,6 +1,8 @@
package conf
import (
"os"
"path/filepath"
"reflect"
"testing"
)
@ -130,3 +132,80 @@ func TestParseConventionalCommit(t *testing.T) {
})
}
}
func TestGetConfPath(t *testing.T) {
tmpDir := t.TempDir()
// 1. Setup layout: tmpDir/alpha/conf.json, tmpDir/conf.json
alphaDir := filepath.Join(tmpDir, "alpha")
if err := os.MkdirAll(alphaDir, 0755); err != nil {
t.Fatalf("failed to create alpha dir: %v", err)
}
alphaConfPath := filepath.Join(alphaDir, "conf.json")
if err := os.WriteFile(alphaConfPath, []byte("{}"), 0644); err != nil {
t.Fatalf("failed to write alpha conf: %v", err)
}
rootConfPath := filepath.Join(tmpDir, "conf.json")
if err := os.WriteFile(rootConfPath, []byte("{}"), 0644); err != nil {
t.Fatalf("failed to write root conf: %v", err)
}
t.Run("Exact Scope Match", func(t *testing.T) {
gotPath, _, cc, err := GetConfPath(tmpDir, "conf.json", "conf.json", "", "alpha")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if gotPath != alphaConfPath {
t.Errorf("got path %s, want %s", gotPath, alphaConfPath)
}
if cc.Scope != "alpha" {
t.Errorf("got scope %s, want alpha", cc.Scope)
}
})
t.Run("Prefix Scope Match for Floating Release", func(t *testing.T) {
gotPath, _, cc, err := GetConfPath(tmpDir, "conf.json", "conf.json", "", "alpha1")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if gotPath != alphaConfPath {
t.Errorf("got path %s, want %s", gotPath, alphaConfPath)
}
if cc.Scope != "alpha" {
t.Errorf("got scope %s, want alpha", cc.Scope)
}
})
t.Run("Fallback to Root Conf when Scope Not Found", func(t *testing.T) {
gotPath, _, cc, err := GetConfPath(tmpDir, "conf.json", "conf.json", "", "beta")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if gotPath != rootConfPath {
t.Errorf("got path %s, want %s", gotPath, rootConfPath)
}
if cc.Scope != "beta" {
t.Errorf("got scope %s, want beta", cc.Scope)
}
})
t.Run("Fallback to Root Conf with Empty Fallback Conf Name", func(t *testing.T) {
gotPath, _, _, err := GetConfPath(tmpDir, "conf.json", "", "", "gamma")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if gotPath != rootConfPath {
t.Errorf("got path %s, want %s", gotPath, rootConfPath)
}
})
t.Run("No Conf Found Error", func(t *testing.T) {
emptyTmpDir := t.TempDir()
_, _, _, err := GetConfPath(emptyTmpDir, "conf.json", "conf.json", "", "alpha")
if err == nil {
t.Fatalf("expected error when no conf found, got nil")
}
})
}

10
go.mod
View File

@ -28,7 +28,7 @@ require (
github.com/fatih/camelcase v1.0.0 // indirect
github.com/fatih/structs v1.1.0 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.6.2 // indirect
github.com/go-git/go-billy/v5 v5.8.0 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/kevinburke/ssh_config v1.4.0 // indirect
@ -37,10 +37,10 @@ require (
github.com/sergi/go-diff v1.4.0 // indirect
github.com/skeema/knownhosts v1.3.1 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
golang.org/x/crypto v0.46.0 // indirect
golang.org/x/net v0.48.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/text v0.32.0 // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/net v0.49.0 // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/text v0.34.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect

24
go.sum
View File

@ -36,8 +36,8 @@ github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM=
github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU=
github.com/go-git/go-billy/v5 v5.8.0 h1:I8hjc3LbBlXTtVuFNJuwYuMiHvQJDq1AT6u4DwDzZG0=
github.com/go-git/go-billy/v5 v5.8.0/go.mod h1:RpvI/rw4Vr5QA+Z60c6d6LXH0rYJo0uD5SqfmrrheCY=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
github.com/go-git/go-git/v5 v5.16.5 h1:mdkuqblwr57kVfXri5TTH+nMFLNUxIj9Z7F5ykFbw5s=
@ -113,27 +113,27 @@ go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5w
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=