Skip to content

Commit 3fa445c

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 3fa445c

23 files changed

Lines changed: 1363 additions & 36 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

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+
}

internal/output/lefthook/hook.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
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+
// Pre-commit stage keys: stable names for Hook.Group so multiple project blocks
8+
// can append jobs to the same ordered group. Each value is both the lookup key
9+
// and the group's emitted `name:`.
10+
const (
11+
// PreCommitFixStage is stage 1: mutating formatters/generators, re-staged via stage_fixed.
12+
PreCommitFixStage = "fix"
13+
// PreCommitLintStage is stage 2: verification, runs after the fix stage.
14+
PreCommitLintStage = "lint"
15+
16+
// HookGroupPreCommit is the lefthook "pre-commit" hook name.
17+
HookGroupPreCommit = "pre-commit"
18+
// HookGroupCommitMsg is the lefthook "commit-msg" hook name.
19+
HookGroupCommitMsg = "commit-msg"
20+
)
21+
22+
// Hook represents the configuration for a single git hook (e.g. pre-commit)
23+
// inside lefthook.yml. A hook can declare either Commands (a named map) or
24+
// Jobs (an ordered list with nested groups) — see lefthook docs for the
25+
// trade-off; mixing both in a single hook is generally not recommended.
26+
type Hook struct { //nolint:govet
27+
// Parallel is a pointer so an explicit `parallel: false` can be emitted
28+
// (with a plain bool + omitempty the false zero-value would be suppressed).
29+
// Only meaningful for Commands-style hooks; Jobs-style hooks control
30+
// parallelism per-Group.
31+
Parallel *bool `yaml:"parallel,omitempty"`
32+
Piped bool `yaml:"piped,omitempty"`
33+
Commands map[string]*Command `yaml:"commands,omitempty"`
34+
Jobs []*Job `yaml:"jobs,omitempty"`
35+
}
36+
37+
// WithParallel sets the hook-level parallel flag (Commands-style hooks only).
38+
func (h *Hook) WithParallel(parallel bool) *Hook {
39+
h.Parallel = &parallel
40+
41+
return h
42+
}
43+
44+
// WithPiped enables piped (sequential, fail-fast) execution of this hook's commands.
45+
func (h *Hook) WithPiped(piped bool) *Hook {
46+
h.Piped = piped
47+
48+
return h
49+
}
50+
51+
// Command returns the named command on this hook, creating it on first access.
52+
func (h *Hook) Command(name string) *Command {
53+
if c, ok := h.Commands[name]; ok {
54+
return c
55+
}
56+
57+
if h.Commands == nil {
58+
h.Commands = map[string]*Command{}
59+
}
60+
61+
c := &Command{}
62+
h.Commands[name] = c
63+
64+
return c
65+
}
66+
67+
// Job appends a new job to this hook's Jobs list and returns it for further
68+
// configuration. Each top-level entry runs in declaration order; use AsGroup
69+
// on the returned job to nest a parallel/sequential group of inner jobs.
70+
func (h *Hook) Job() *Job {
71+
j := &Job{}
72+
73+
h.Jobs = append(h.Jobs, j)
74+
75+
return j
76+
}
77+
78+
// Group returns the named group on this hook, wrapped in a Job entry, creating
79+
// it on first access (mirroring makefile.Output.Target). The name is emitted as
80+
// the wrapping job's `name:` and doubles as the lookup key, so different project
81+
// blocks can contribute jobs to the same group regardless of compile order;
82+
// group emission order follows first-creation order.
83+
func (h *Hook) Group(name string) *Group {
84+
for _, j := range h.Jobs {
85+
if j.Group != nil && j.Name == name {
86+
return j.Group
87+
}
88+
}
89+
90+
g := &Group{}
91+
92+
h.Jobs = append(h.Jobs, &Job{Name: name, Group: g})
93+
94+
return g
95+
}

0 commit comments

Comments
 (0)