Skip to content

Commit 5c3b596

Browse files
committed
feat: add lefthook support
Add support for lefthook. Executes fmt, generate, lint, and conformance make targets on commit (at different stages). Signed-off-by: Maja Bojarska <maja.bojarska@siderolabs.com>
1 parent 8085f7d commit 5c3b596

32 files changed

Lines changed: 1419 additions & 58 deletions

Makefile

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# THIS FILE WAS AUTOMATICALLY GENERATED BY KRES, PLEASE DO NOT EDIT.
22
#
3-
# Generated on 2026-05-26T16:46:25Z by kres c267185-dirty.
3+
# Generated on 2026-05-28T15:40:14Z by kres 80fec4e.
44

55
# common variables
66

@@ -214,7 +214,7 @@ lint-gofumpt: ## Runs gofumpt linter.
214214

215215
.PHONY: fmt
216216
fmt: ## Formats the source code
217-
@docker run --rm -it -v $(PWD):/src -w /src golang:$(GO_VERSION) \
217+
@docker run --rm -v $(PWD):/src -w /src golang:$(GO_VERSION) \
218218
bash -c "export GOTOOLCHAIN=local; \
219219
export GO111MODULE=on; export GOPROXY=https://proxy.golang.org; \
220220
go install mvdan.cc/gofumpt@$(GOFUMPT_VERSION) && \
@@ -335,7 +335,7 @@ release-notes: $(ARTIFACTS)
335335
.PHONY: conformance
336336
conformance:
337337
@docker pull $(CONFORMANCE_IMAGE)
338-
@docker run --rm -it -v $(PWD):/src -w /src $(CONFORMANCE_IMAGE) enforce
338+
@docker run --rm -v $(PWD):/src -w /src $(CONFORMANCE_IMAGE) enforce
339339

340340
.PHONY: renovate-local
341341
renovate-local: ## runs renovate locally to check syntax and test configuration

cmd/kres/cmd/gen.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"github.com/siderolabs/kres/internal/output/github"
2020
"github.com/siderolabs/kres/internal/output/gitignore"
2121
"github.com/siderolabs/kres/internal/output/golangci"
22+
"github.com/siderolabs/kres/internal/output/lefthook"
2223
"github.com/siderolabs/kres/internal/output/license"
2324
"github.com/siderolabs/kres/internal/output/makefile"
2425
"github.com/siderolabs/kres/internal/output/markdownlint"
@@ -91,6 +92,7 @@ func runGen() error {
9192
output.Wrap(release.NewOutput()),
9293
output.Wrap(markdownlint.NewOutput()),
9394
output.Wrap(template.NewOutput()),
95+
output.Wrap(lefthook.NewOutput()),
9496
)
9597
}
9698

internal/output/codecov/codecov.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import (
1414
)
1515

1616
const (
17-
filename = ".codecov.yml"
17+
configFile = ".codecov.yml"
1818
)
1919

2020
//go:embed codecov.yml
@@ -60,13 +60,13 @@ func (o *Output) Filenames() []string {
6060
return nil
6161
}
6262

63-
return []string{filename}
63+
return []string{configFile}
6464
}
6565

6666
// GenerateFile implements output.FileWriter interface.
6767
func (o *Output) GenerateFile(filename string, w io.Writer) error {
6868
switch filename {
69-
case filename:
69+
case configFile:
7070
return o.config(w)
7171
default:
7272
panic("unexpected filename: " + filename)

internal/output/conform/conform.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import (
1717
)
1818

1919
const (
20-
filename = ".conform.yaml"
20+
configFile = ".conform.yaml"
2121
)
2222

2323
// Output implements .conform.yaml generation.
@@ -96,13 +96,13 @@ func (o *Output) Filenames() []string {
9696
return nil
9797
}
9898

99-
return []string{filename}
99+
return []string{configFile}
100100
}
101101

102102
// GenerateFile implements output.FileWriter interface.
103103
func (o *Output) GenerateFile(filename string, w io.Writer) error {
104104
switch filename {
105-
case filename:
105+
case configFile:
106106
return o.config(w)
107107
default:
108108
panic("unexpected filename: " + filename)

internal/output/dockerfile/dockerfile.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ import (
1717
)
1818

1919
const (
20-
filename = "Dockerfile"
21-
syntax = "docker/dockerfile-upstream:" + config.DockerfileFrontendImageVersion
20+
configFile = "Dockerfile"
21+
syntax = "docker/dockerfile-upstream:" + config.DockerfileFrontendImageVersion
2222
)
2323

2424
// Output implements Dockerfile and .dockerignore generation.
@@ -55,13 +55,13 @@ func (o *Output) Filenames() []string {
5555
return nil
5656
}
5757

58-
return []string{filename}
58+
return []string{configFile}
5959
}
6060

6161
// GenerateFile implements output.FileWriter interface.
6262
func (o *Output) GenerateFile(filename string, w io.Writer) error {
6363
switch filename {
64-
case filename:
64+
case configFile:
6565
return o.dockerfile(w)
6666
default:
6767
panic("unexpected filename: " + filename)

internal/output/dockerignore/dockerignore.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import (
1313
)
1414

1515
const (
16-
filename = ".dockerignore"
16+
configFile = ".dockerignore"
1717
)
1818

1919
// Output implements .dockerignore generation.
@@ -39,7 +39,7 @@ func (o *Output) Compile(compiler Compiler) error {
3939

4040
// Filenames implements output.FileWriter interface.
4141
func (o *Output) Filenames() []string {
42-
return []string{filename}
42+
return []string{configFile}
4343
}
4444

4545
// AllowLocalPath adds path to the list of paths to be copied into the context.
@@ -52,7 +52,7 @@ func (o *Output) AllowLocalPath(paths ...string) *Output {
5252
// GenerateFile implements output.FileWriter interface.
5353
func (o *Output) GenerateFile(filename string, w io.Writer) error {
5454
switch filename {
55-
case filename:
55+
case configFile:
5656
return o.dockerignore(w)
5757
default:
5858
panic("unexpected filename: " + filename)

internal/output/gitignore/gitignore.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import (
1313
)
1414

1515
const (
16-
filename = ".gitignore"
16+
configFile = ".gitignore"
1717
)
1818

1919
// Output implements .gitignore generation.
@@ -44,13 +44,13 @@ func (o *Output) IgnorePath(paths ...string) {
4444

4545
// Filenames implements output.FileWriter interface.
4646
func (o *Output) Filenames() []string {
47-
return []string{filename}
47+
return []string{configFile}
4848
}
4949

5050
// GenerateFile implements output.FileWriter interface.
5151
func (o *Output) GenerateFile(filename string, w io.Writer) error {
5252
switch filename {
53-
case filename:
53+
case configFile:
5454
return o.gitignore(w)
5555
default:
5656
panic("unexpected filename: " + filename)
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
// This Source Code Form is subject to the terms of the Mozilla Public
2+
// License, v. 2.0. If a copy of the MPL was not distributed with this
3+
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
4+
5+
package lefthook
6+
7+
// Command represents a single named command under a hook's commands: map.
8+
type Command struct { //nolint:govet
9+
Run string `yaml:"run"`
10+
Tags []string `yaml:"tags,omitempty"`
11+
Glob string `yaml:"glob,omitempty"`
12+
Files string `yaml:"files,omitempty"`
13+
Skip []string `yaml:"skip,omitempty"`
14+
Only []string `yaml:"only,omitempty"`
15+
Interactive bool `yaml:"interactive,omitempty"`
16+
StageFixed bool `yaml:"stage_fixed,omitempty"`
17+
Priority int `yaml:"priority,omitempty"`
18+
}
19+
20+
// WithRun sets the shell command lefthook executes for this command.
21+
func (c *Command) WithRun(run string) *Command {
22+
c.Run = run
23+
24+
return c
25+
}
26+
27+
// WithTags attaches tags used for selective hook execution (lefthook --tags ...).
28+
func (c *Command) WithTags(tags ...string) *Command {
29+
c.Tags = tags
30+
31+
return c
32+
}
33+
34+
// WithGlob restricts the command to files matching the given glob.
35+
func (c *Command) WithGlob(glob string) *Command {
36+
c.Glob = glob
37+
38+
return c
39+
}
40+
41+
// WithFiles overrides the default file source (e.g. "git diff --name-only ...").
42+
func (c *Command) WithFiles(files string) *Command {
43+
c.Files = files
44+
45+
return c
46+
}
47+
48+
// WithSkip lists git states or refs where the command should be skipped (e.g. "merge", "rebase").
49+
func (c *Command) WithSkip(skip ...string) *Command {
50+
c.Skip = skip
51+
52+
return c
53+
}
54+
55+
// WithOnly is the inverse of WithSkip: command runs only in the listed states.
56+
func (c *Command) WithOnly(only ...string) *Command {
57+
c.Only = only
58+
59+
return c
60+
}
61+
62+
// WithInteractive marks the command as needing a TTY (stdin/stdout passthrough).
63+
func (c *Command) WithInteractive() *Command {
64+
c.Interactive = true
65+
66+
return c
67+
}
68+
69+
// WithStageFixed re-stages files modified by the command (useful for formatters).
70+
func (c *Command) WithStageFixed() *Command {
71+
c.StageFixed = true
72+
73+
return c
74+
}
75+
76+
// WithPriority sets the command's run order within its hook (lower runs first).
77+
func (c *Command) WithPriority(priority int) *Command {
78+
c.Priority = priority
79+
80+
return c
81+
}

internal/output/lefthook/config.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
// This Source Code Form is subject to the terms of the Mozilla Public
2+
// License, v. 2.0. If a copy of the MPL was not distributed with this
3+
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
4+
5+
package lefthook
6+
7+
// Config is a declarative description of lefthook.yml contributions, suitable
8+
// for unmarshalling from a kres.yaml block.
9+
//
10+
// Hooks reuse the very same builder types (Hook, Command, Job, Group) that
11+
// serialize lefthook.yml, so the config is authored in lefthook's native schema
12+
// and there is a single definitive set of types for both serializing the output
13+
// and deserializing the config.
14+
type Config struct {
15+
Hooks map[string]*Hook `yaml:"hooks"`
16+
Enabled bool `yaml:"enabled"`
17+
}
18+
19+
// Compile merges the configured hooks into the output. It is a no-op when the
20+
// config is disabled.
21+
func (c Config) Compile(output *Output) error {
22+
if !c.Enabled {
23+
return nil
24+
}
25+
26+
for name, cfgHook := range c.Hooks {
27+
if cfgHook == nil {
28+
continue
29+
}
30+
31+
hook := output.Hook(name)
32+
33+
if cfgHook.Parallel != nil {
34+
hook.WithParallel(*cfgHook.Parallel)
35+
}
36+
37+
if cfgHook.Piped {
38+
hook.WithPiped(true)
39+
}
40+
41+
for cmdName, cmd := range cfgHook.Commands {
42+
*hook.Command(cmdName) = *cmd
43+
}
44+
45+
for _, job := range cfgHook.Jobs {
46+
// A named job wrapping a group is merged into the hook's group of
47+
// the same name, so a config can extend the shared fix/lint groups
48+
// emitted by the standard blocks instead of forking a new one.
49+
if job.Group != nil && job.Name != "" {
50+
group := hook.Group(job.Name)
51+
52+
if job.Group.Parallel != nil {
53+
group.WithParallel(*job.Group.Parallel)
54+
}
55+
56+
if job.Group.Piped {
57+
group.WithPiped(true)
58+
}
59+
60+
group.Jobs = append(group.Jobs, job.Group.Jobs...)
61+
62+
continue
63+
}
64+
65+
hook.Jobs = append(hook.Jobs, job)
66+
}
67+
}
68+
69+
return nil
70+
}

internal/output/lefthook/group.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// This Source Code Form is subject to the terms of the Mozilla Public
2+
// License, v. 2.0. If a copy of the MPL was not distributed with this
3+
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
4+
5+
package lefthook
6+
7+
// Group is a container for nested Jobs with its own parallel/piped semantics.
8+
// Used to express "run these in parallel, then those sequentially" without
9+
// touching the hook-level execution model.
10+
type Group struct { //nolint:govet
11+
// Parallel is a pointer so an explicit `parallel: false` can be emitted
12+
// (the bool zero value would otherwise be suppressed by omitempty).
13+
Parallel *bool `yaml:"parallel,omitempty"`
14+
Piped bool `yaml:"piped,omitempty"`
15+
Jobs []*Job `yaml:"jobs,omitempty"`
16+
}
17+
18+
// WithParallel toggles parallel execution of this group's jobs.
19+
func (g *Group) WithParallel(parallel bool) *Group {
20+
g.Parallel = &parallel
21+
22+
return g
23+
}
24+
25+
// WithPiped enables piped (sequential, fail-fast, stdout-chained) execution.
26+
func (g *Group) WithPiped(piped bool) *Group {
27+
g.Piped = piped
28+
29+
return g
30+
}
31+
32+
// Job appends a new job to the group and returns it for further configuration.
33+
func (g *Group) Job() *Job {
34+
j := &Job{}
35+
36+
g.Jobs = append(g.Jobs, j)
37+
38+
return j
39+
}

0 commit comments

Comments
 (0)