Skip to content
Merged
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
21 changes: 21 additions & 0 deletions .commitlint-scope.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#$schema: https://github.com/thumbrise/commitlint-scope/blob/main/docs/schema/config.v3.json

# Scope parsing customization. Not required, if you follow common conventional header. In example: 'type!(scope): subject'
#scopeRegex: ^[a-z]+(?:\((?P<scope>[^)]+)\))?!?:\s

# Patterns list: each item specifies a list of scopes and the corresponding file glob patterns.
patterns:
# - scopes: ["auth"]
# files: ["services/auth/**"]
#
# - scopes: ["migrations", "sql"]
# files: ["database/migrations/*.sql"]
#
# - scopes: ["frontend", "assets"]
# files: ["**/assets/**", "**/frontend/**"]
#
# - scopes: ["docs", "md"]
# files: ["**/*.md"]
#
# - scopes: ["some.dot.scope", "any-anotherscope"]
# files: ["**/rail.v1.json"]
21 changes: 20 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ jobs:
args: --help

- name: Run lint
run: task lint
run: golangci-lint run

commitlint:
name: Lint commit messages
Expand All @@ -59,6 +59,25 @@ jobs:
- name: Validate PR commits with commitlint
run: npx commitlint --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }} --verbose

commitlint-scope:
name: Lint commit scopes
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0

- name: Lint commit scopes
run: |
docker run --rm \
-v "${{ github.workspace }}:/repo" \
-w /repo \
thumbrise/commitlint-scope:latest-alpine \
run \
--from ${{ github.event.pull_request.base.sha }} \
--to ${{ github.event.pull_request.head.sha }} \
--verbose
Comment on lines +66 to +79
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE=".github/workflows/ci.yml"
if [ ! -f "$FILE" ]; then
  echo "Missing $FILE" >&2
  exit 1
fi

echo "== File: $FILE =="
python3 - <<'PY'
import itertools
path=".github/workflows/ci.yml"
with open(path,'r',encoding='utf-8') as f:
    lines=f.readlines()
for i in range(1, len(lines)+1):
    if 60 <= i <= 95:
        print(f"{i:4d}: {lines[i-1].rstrip()}")
PY

echo
echo "== Extract relevant uses/run lines =="
rg -n "actions/checkout@|persist-credentials|commitlint-scope:" "$FILE" || true

Repository: thumbrise/commitlint-scope

Length of output: 1530


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE=".github/workflows/ci.yml"
echo "== Search persist-credentials =="
rg -n "persist-credentials" "$FILE" || true

echo
echo "== Search all commitlint-scope image tags =="
rg -n "thumbrise/commitlint-scope:" "$FILE" || true

Repository: thumbrise/commitlint-scope

Length of output: 207


🌐 Web query:

actions/checkout persist-credentials default true

💡 Result:

In GitHub Actions, the actions/checkout input persist-credentials defaults to true (i.e., credentials are persisted unless you set persist-credentials: false). [1][2] What “persist-credentials” does: - When persist-credentials is enabled, actions/checkout stores the auth token in the local git config so later steps can run authenticated git commands; the token is removed during post-job cleanup. [2][1] How to opt out: - Set persist-credentials: false on the checkout step where you don’t need authenticated git operations (e.g., read-only workflows). [2][3]

Citations:


Harden commitlint-scope job supply-chain + token hygiene

  • Pin actions/checkout@v6 to a full commit SHA (line 66) instead of a moving tag.
  • Replace thumbrise/commitlint-scope:latest-alpine (line 75) with an immutable image digest.
  • Disable checkout credential persistence by adding persist-credentials: false to the checkout step (currently unset, so it defaults to true).
🧰 Tools
🪛 zizmor (1.25.2)

[warning] 66-68: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[error] 66-66: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci.yml around lines 66 - 79, Update the "actions/checkout"
step to use a fixed commit SHA instead of the v6 tag and add
persist-credentials: false to disable credential persistence; in the "Lint
commit scopes" step replace the mutable Docker image
thumbrise/commitlint-scope:latest-alpine with its immutable image digest
(sha256@...) so the run uses a pinned image; ensure these changes are applied in
the checkout step and the Lint commit scopes docker run invocation (the two
unique identifiers to edit are the actions/checkout usage and the
thumbrise/commitlint-scope image reference).


test:
name: Test
runs-on: ubuntu-latest
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ docker run --rm -v "$(pwd):/repo" -w /repo thumbrise/commitlint-scope:latest-alp

### Init

Generate .commitlint-scope.yml file.
Generate .commitlint-scope.yaml file.

```shell
commitlint-scope init
Expand Down
1 change: 1 addition & 0 deletions Taskfile.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ tasks:
lint:
desc: Run lint with auto-fix where possible (dev workflow)
cmds:
- docker run --rm -v "$(pwd):/repo" -w /repo thumbrise/commitlint-scope:latest-alpine run --from main --to HEAD
- golangci-lint run --fix

test:
Expand Down
2 changes: 1 addition & 1 deletion cmd/commands/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ patterns:
`

const (
InitConfigFileName = validator.ConfigName + ".yml"
InitConfigFileName = validator.ConfigName + ".yaml"
InitConfigFileMode = 0o600
)

Expand Down
13 changes: 3 additions & 10 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ require (
github.com/fatih/color v1.19.0
github.com/go-viper/mapstructure/v2 v2.5.0
github.com/gobwas/glob v0.2.3
github.com/spf13/viper v1.21.0
github.com/knadh/koanf/parsers/yaml v1.1.0
github.com/knadh/koanf/providers/file v1.2.1
github.com/knadh/koanf/v2 v2.3.2
github.com/stretchr/testify v1.11.1
github.com/urfave/cli/v3 v3.9.0
)
Expand All @@ -21,28 +23,19 @@ require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jedib0t/go-pretty/v6 v6.7.8 // indirect
github.com/knadh/koanf/maps v0.1.2 // indirect
github.com/knadh/koanf/parsers/yaml v1.1.0 // indirect
github.com/knadh/koanf/providers/env v1.1.0 // indirect
github.com/knadh/koanf/providers/file v1.2.1 // indirect
github.com/knadh/koanf/providers/posflag v1.0.1 // indirect
github.com/knadh/koanf/providers/structs v1.0.0 // indirect
github.com/knadh/koanf/v2 v2.3.2 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.19 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/rs/zerolog v1.34.0 // indirect
github.com/sagikazarmark/locafero v0.11.0 // indirect
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/spf13/cobra v1.10.2 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/stretchr/objx v0.5.3 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/vektra/mockery/v3 v3.7.0 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
Expand Down
16 changes: 0 additions & 16 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
Expand Down Expand Up @@ -59,8 +57,6 @@ github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa1
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
Expand All @@ -71,29 +67,17 @@ github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/urfave/cli/v3 v3.9.0 h1:AV9lIiPv3ukYnxunaCUsHnEozptYmDN2F0+yWqLMn/c=
github.com/urfave/cli/v3 v3.9.0/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
github.com/vektra/mockery/v3 v3.7.0 h1:Dd0EeaOcRJBVP9n3oYOVPV7KdPaaE3EcwTppaZIsFSM=
Expand Down
63 changes: 36 additions & 27 deletions pkg/validator/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,61 +3,70 @@ package validator
import (
"errors"
"fmt"
"os"
"reflect"
"regexp"

"github.com/go-viper/mapstructure/v2"
"github.com/spf13/viper"
"github.com/knadh/koanf/parsers/yaml"
"github.com/knadh/koanf/providers/file"
"github.com/knadh/koanf/v2"
)

const ConfigName = ".commitlint-scope"
const ConfigName = ".commitlint-scope.yaml"
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep the config filename contract consistent.

Line 16 turns ConfigName into a full filename, but cmd/commands/init.go:42-45 still appends ".yml". That means init now writes .commitlint-scope.yaml.yml, while LoadConfig() only reads .commitlint-scope.yaml. It also drops support for the old .commitlint-scope filename, so existing repos can silently fall back to defaults. Please either update the downstream filename construction and add a legacy fallback here, or keep a single shared basename/filename contract.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/validator/config.go` at line 16, ConfigName was turned into a full
filename which conflicts with cmd/commands/init.go appending ".yml" and drops
legacy support; change ConfigName back to the basename ".commitlint-scope" and
update LoadConfig() to look for ConfigName+".yaml", then ConfigName+".yml", then
the legacy ConfigName (no extension) so both init (which can continue using
ConfigName+".yml") and existing repos are supported. Ensure references use the
ConfigName symbol and keep the search order so ".yaml" takes precedence, then
".yml", then the legacy filename.


type PatternItem struct {
Scopes []string `mapstructure:"scopes"`
Files []string `mapstructure:"files"`
Scopes []string `koanf:"scopes"`
Files []string `koanf:"files"`
}

type Config struct {
ScopeRegex *regexp.Regexp `mapstructure:"scopeRegex"`
Patterns []PatternItem `mapstructure:"patterns"`
ScopeRegex *regexp.Regexp `koanf:"scopeRegex"`
Patterns []PatternItem `koanf:"patterns"`
}

var ErrConfigRead = errors.New("error reading config")
var (
ErrConfigRead = errors.New("error reading config")
ErrRegexDecode = errors.New("expected string for regexp decode")
)

func LoadConfig() (Config, error) {
v := viper.New()
v.SetConfigName(ConfigName)
v.AddConfigPath(".")
v.SetDefault("scopeRegex", regexp.MustCompile(`^[a-z]+(?:\((?P<scope>[^)]+)\))?!?:\s`))
k := koanf.New(".")

var cfg Config
defaultRegex := `^[a-z]+(?:\((?P<scope>[^)]+)\))?!?:\s`
_ = k.Set("scopeRegex", defaultRegex)

if err := v.ReadInConfig(); err != nil {
if _, ok := errors.AsType[viper.ConfigFileNotFoundError](err); !ok {
if err := k.Load(file.Provider(ConfigName), yaml.Parser()); err != nil {
if !os.IsNotExist(err) {
return Config{}, fmt.Errorf("%w: %w", ErrConfigRead, err)
}
}

if err := v.Unmarshal(&cfg, regexDecode); err != nil {
var cfg Config

unmarshalConf := koanf.UnmarshalConf{
DecoderConfig: &mapstructure.DecoderConfig{
Result: &cfg,
DecodeHook: regexDecodeHook,
},
}

if err := k.UnmarshalWithConf("", &cfg, unmarshalConf); err != nil {
return Config{}, fmt.Errorf("%w: %w", ErrConfigRead, err)
}

return cfg, nil
}

var ErrRegexDecode = errors.New("expected string for regexp decode")

func regexDecode(cfg *mapstructure.DecoderConfig) {
cfg.DecodeHook = func(from reflect.Type, to reflect.Type, data interface{}) (interface{}, error) {
if from.Kind() == reflect.String && to == reflect.TypeOf(&regexp.Regexp{}) {
val, ok := data.(string)
if !ok {
return nil, fmt.Errorf("%w got %T", ErrRegexDecode, data)
}

return regexp.Compile(val)
func regexDecodeHook(from reflect.Type, to reflect.Type, data interface{}) (interface{}, error) {
if from.Kind() == reflect.String && to == reflect.TypeOf(&regexp.Regexp{}) {
val, ok := data.(string)
if !ok {
return nil, fmt.Errorf("%w got %T", ErrRegexDecode, data)
}

return data, nil
return regexp.Compile(val)
}

return data, nil
}
2 changes: 1 addition & 1 deletion pkg/validator/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ patterns:
require.NoError(t, os.Chdir(dir))

if tt.yaml != "" {
err = os.WriteFile(filepath.Join(dir, ".commitlint-scope.yaml"), []byte(tt.yaml), 0o644)
err = os.WriteFile(filepath.Join(dir, validator.ConfigName), []byte(tt.yaml), 0o644)
require.NoError(t, err)
}

Expand Down
Loading