From edf5c617be48b057e7a4638cbbd2fcc939e560f9 Mon Sep 17 00:00:00 2001 From: BoYanZh Date: Tue, 28 Jul 2026 00:56:44 -0700 Subject: [PATCH 1/2] feat(conf): support prefix scope matching and release config fallback --- cmd/joj3/conf/conf.go | 131 +++++++++++++++++++++++++++---------- cmd/joj3/conf/conf_test.go | 79 ++++++++++++++++++++++ 2 files changed, 177 insertions(+), 33 deletions(-) diff --git a/cmd/joj3/conf/conf.go b/cmd/joj3/conf/conf.go index ac1a90b..aa9196c 100644 --- a/cmd/joj3/conf/conf.go +++ b/cmd/joj3/conf/conf.go @@ -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 { diff --git a/cmd/joj3/conf/conf_test.go b/cmd/joj3/conf/conf_test.go index a52f737..c436c1c 100644 --- a/cmd/joj3/conf/conf_test.go +++ b/cmd/joj3/conf/conf_test.go @@ -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") + } + }) +} + -- 2.30.2 From c3269e8056462e784d59144a6da09421c3e8f1b4 Mon Sep 17 00:00:00 2001 From: BoYanZh Date: Tue, 28 Jul 2026 02:49:39 -0700 Subject: [PATCH 2/2] chore(deps): upgrade dependencies for Go 1.24 compatibility without modifying go-judge/pb --- go.mod | 10 +++++----- go.sum | 24 ++++++++++++------------ 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/go.mod b/go.mod index fe3dd71..50c73e3 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 549ae31..6d7839e 100644 --- a/go.sum +++ b/go.sum @@ -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= -- 2.30.2