Skip to content

Commit 3e7c099

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 e1a258d commit 3e7c099

12 files changed

Lines changed: 741 additions & 5 deletions

File tree

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"
@@ -76,6 +77,7 @@ func runGen() error {
7677
output.Wrap(sops.NewOutput()),
7778
output.Wrap(renovate.NewOutput()),
7879
output.Wrap(conform.NewOutput()),
80+
output.Wrap(lefthook.NewOutput()),
7981
}
8082

8183
if !options.CompileGithubWorkflowsOnly {
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/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: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
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+
// Hook represents the configuration for a single git hook (e.g. pre-commit)
8+
// inside lefthook.yml. A hook can declare either Commands (a named map) or
9+
// Jobs (an ordered list with nested groups) — see lefthook docs for the
10+
// trade-off; mixing both in a single hook is generally not recommended.
11+
type Hook struct { //nolint:govet
12+
// Parallel is a pointer so an explicit `parallel: false` can be emitted
13+
// (with a plain bool + omitempty the false zero-value would be suppressed).
14+
// Only meaningful for Commands-style hooks; Jobs-style hooks control
15+
// parallelism per-Group.
16+
Parallel *bool `yaml:"parallel,omitempty"`
17+
Piped bool `yaml:"piped,omitempty"`
18+
Commands map[string]*Command `yaml:"commands,omitempty"`
19+
Jobs []*Job `yaml:"jobs,omitempty"`
20+
}
21+
22+
// WithParallel sets the hook-level parallel flag (Commands-style hooks only).
23+
func (h *Hook) WithParallel(parallel bool) *Hook {
24+
h.Parallel = &parallel
25+
26+
return h
27+
}
28+
29+
// WithPiped enables piped (sequential, fail-fast) execution of this hook's commands.
30+
func (h *Hook) WithPiped(piped bool) *Hook {
31+
h.Piped = piped
32+
33+
return h
34+
}
35+
36+
// Command returns the named command on this hook, creating it on first access.
37+
func (h *Hook) Command(name string) *Command {
38+
if c, ok := h.Commands[name]; ok {
39+
return c
40+
}
41+
42+
if h.Commands == nil {
43+
h.Commands = map[string]*Command{}
44+
}
45+
46+
c := &Command{}
47+
h.Commands[name] = c
48+
49+
return c
50+
}
51+
52+
// Job appends a new job to this hook's Jobs list and returns it for further
53+
// configuration. Each top-level entry runs in declaration order; use AsGroup
54+
// on the returned job to nest a parallel/sequential group of inner jobs.
55+
func (h *Hook) Job() *Job {
56+
j := &Job{}
57+
58+
h.Jobs = append(h.Jobs, j)
59+
60+
return j
61+
}

internal/output/lefthook/job.go

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
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+
// Job is an entry under a hook's or group's jobs: list. Each Job either runs
8+
// a command directly (via Run or Script) or wraps a nested Group.
9+
type Job struct { //nolint:govet
10+
Name string `yaml:"name,omitempty"`
11+
Run string `yaml:"run,omitempty"`
12+
Script string `yaml:"script,omitempty"`
13+
Tags []string `yaml:"tags,omitempty"`
14+
Glob []string `yaml:"glob,omitempty"`
15+
Exclude []string `yaml:"exclude,omitempty"`
16+
Root string `yaml:"root,omitempty"`
17+
Env map[string]string `yaml:"env,omitempty"`
18+
Skip []string `yaml:"skip,omitempty"`
19+
Only []string `yaml:"only,omitempty"`
20+
Interactive bool `yaml:"interactive,omitempty"`
21+
StageFixed bool `yaml:"stage_fixed,omitempty"`
22+
Priority int `yaml:"priority,omitempty"`
23+
Group *Group `yaml:"group,omitempty"`
24+
}
25+
26+
// WithName sets the job's display name.
27+
func (j *Job) WithName(name string) *Job {
28+
j.Name = name
29+
30+
return j
31+
}
32+
33+
// WithRun sets the shell command to execute for this job.
34+
func (j *Job) WithRun(run string) *Job {
35+
j.Run = run
36+
37+
return j
38+
}
39+
40+
// WithScript sets a script file to execute (relative to lefthook source_dir).
41+
func (j *Job) WithScript(script string) *Job {
42+
j.Script = script
43+
44+
return j
45+
}
46+
47+
// WithTags attaches selectable tags (lefthook --tags ...).
48+
func (j *Job) WithTags(tags ...string) *Job {
49+
j.Tags = tags
50+
51+
return j
52+
}
53+
54+
// WithGlob restricts the job to files matching the given glob(s).
55+
func (j *Job) WithGlob(glob ...string) *Job {
56+
j.Glob = glob
57+
58+
return j
59+
}
60+
61+
// WithExclude is the inverse of WithGlob: skip files matching these patterns.
62+
func (j *Job) WithExclude(exclude ...string) *Job {
63+
j.Exclude = exclude
64+
65+
return j
66+
}
67+
68+
// WithRoot changes the working directory for the job.
69+
func (j *Job) WithRoot(root string) *Job {
70+
j.Root = root
71+
72+
return j
73+
}
74+
75+
// WithEnv sets an environment variable on the job; safe to call multiple times.
76+
func (j *Job) WithEnv(name, value string) *Job {
77+
if j.Env == nil {
78+
j.Env = map[string]string{}
79+
}
80+
81+
j.Env[name] = value
82+
83+
return j
84+
}
85+
86+
// WithSkip lists git states or refs where the job should be skipped (e.g. "merge", "rebase").
87+
func (j *Job) WithSkip(skip ...string) *Job {
88+
j.Skip = skip
89+
90+
return j
91+
}
92+
93+
// WithOnly is the inverse of WithSkip: job runs only in the listed states.
94+
func (j *Job) WithOnly(only ...string) *Job {
95+
j.Only = only
96+
97+
return j
98+
}
99+
100+
// WithInteractive marks the job as needing a TTY (stdin/stdout passthrough).
101+
func (j *Job) WithInteractive() *Job {
102+
j.Interactive = true
103+
104+
return j
105+
}
106+
107+
// WithStageFixed re-stages files modified by the job (useful for formatters).
108+
func (j *Job) WithStageFixed() *Job {
109+
j.StageFixed = true
110+
111+
return j
112+
}
113+
114+
// WithPriority sets the job's run order within its container (lower runs first).
115+
func (j *Job) WithPriority(priority int) *Job {
116+
j.Priority = priority
117+
118+
return j
119+
}
120+
121+
// AsGroup turns this job into a container for a nested Group, creating the
122+
// group on first call and returning it for chained configuration. The job's
123+
// Run/Script fields are typically left empty when AsGroup is used.
124+
func (j *Job) AsGroup() *Group {
125+
if j.Group != nil {
126+
return j.Group
127+
}
128+
129+
j.Group = &Group{}
130+
131+
return j.Group
132+
}

0 commit comments

Comments
 (0)