Skip to content

Commit c844058

Browse files
committed
add checksum_cmd directive to command
1 parent 568bc46 commit c844058

14 files changed

Lines changed: 244 additions & 16 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,4 @@ _lets
2626
coverage.out
2727
node_modules
2828
TODO
29+
.DS_Store

checksum/checksum.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ import (
55
"crypto/sha1"
66
"fmt"
77
"os"
8+
"os/exec"
89
"path/filepath"
910
"sort"
11+
"strings"
1012

1113
"github.com/lets-cli/lets/set"
1214
"github.com/lets-cli/lets/util"
@@ -99,6 +101,19 @@ func getChecksumsKeys(mapping map[string][]string) []string {
99101
return keys
100102
}
101103

104+
func CalculateChecksumFromCmd(shell string, workDir string, script string) (string, error) {
105+
cmd := exec.Command(shell, "-c", script)
106+
cmd.Dir = workDir
107+
108+
out, err := cmd.Output()
109+
if err != nil {
110+
return "", fmt.Errorf("can not calculate checksum from cmd: %s: %w", script, err)
111+
}
112+
113+
res := string(out)
114+
return strings.TrimSpace(res), nil
115+
}
116+
102117
// CalculateChecksumFromSources calculates checksum from checksumSources.
103118
func CalculateChecksumFromSources(workDir string, checksumSources map[string][]string) (map[string]string, error) {
104119
checksumMap := make(map[string]string)

checksum/checksum_test.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package checksum
33
import (
44
"fmt"
55
"os"
6+
"path/filepath"
67
"testing"
78

89
"github.com/lets-cli/lets/test"
@@ -136,3 +137,21 @@ func TestCalculateChecksumFromListOrMap(t *testing.T) {
136137
)
137138
}
138139
}
140+
141+
func TestCalculateChecksumFromCmdUsesWorkDir(t *testing.T) {
142+
tempDir := t.TempDir()
143+
checksumFilePath := filepath.Join(tempDir, "checksum.txt")
144+
145+
if err := os.WriteFile(checksumFilePath, []byte("checksum-from-workdir"), 0o600); err != nil {
146+
t.Fatalf("can not write checksum file. Error: %s", err)
147+
}
148+
149+
checksumResult, err := CalculateChecksumFromCmd("sh", tempDir, "cat checksum.txt")
150+
if err != nil {
151+
t.Fatalf("checksum command failed. Error: %s", err)
152+
}
153+
154+
if checksumResult != "checksum-from-workdir" {
155+
t.Fatalf("wrong checksum output. Expect: %s, got: %s", "checksum-from-workdir", checksumResult)
156+
}
157+
}

config/config/command.go

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ type Command struct {
3333
Depends *Deps
3434
ChecksumMap map[string]string
3535
PersistChecksum bool
36+
ChecksumCmd string
3637
// args from 'lets run --debug' will become [--debug]
3738
Args []string
3839

@@ -68,7 +69,8 @@ func (c *Command) UnmarshalYAML(unmarshal func(interface{}) error) error {
6869
After string
6970
Ref string
7071
Checksum *Checksum
71-
PersistChecksum bool `yaml:"persist_checksum"`
72+
ChecksumCmd string `yaml:"checksum_cmd"`
73+
PersistChecksum bool `yaml:"persist_checksum"`
7274
}
7375

7476
if err := unmarshal(&cmd); err != nil {
@@ -109,9 +111,13 @@ func (c *Command) UnmarshalYAML(unmarshal func(interface{}) error) error {
109111
c.ChecksumSources = *cmd.Checksum
110112
}
111113

114+
if cmd.ChecksumCmd != "" {
115+
c.ChecksumCmd = cmd.ChecksumCmd
116+
}
117+
112118
c.PersistChecksum = cmd.PersistChecksum
113-
if len(c.ChecksumSources) == 0 && c.PersistChecksum {
114-
return errors.New("'persist_checksum' must be used with 'checksum'")
119+
if len(c.ChecksumSources) == 0 && c.ChecksumCmd == "" && c.PersistChecksum {
120+
return errors.New("'persist_checksum' must be used with 'checksum' or 'checksum_cmd'")
115121
}
116122

117123
if cmd.Ref != "" {
@@ -154,6 +160,7 @@ func (c *Command) Clone() *Command {
154160
Depends: c.Depends.Clone(),
155161
ChecksumMap: cloneMap(c.ChecksumMap),
156162
PersistChecksum: c.PersistChecksum,
163+
ChecksumCmd: c.ChecksumCmd,
157164
ChecksumSources: cloneMapSlice(c.ChecksumSources),
158165
persistedChecksums: cloneMap(c.persistedChecksums),
159166
Args: cloneSlice(c.Args),
@@ -190,7 +197,17 @@ func (c *Command) Help() string {
190197
return strings.TrimSuffix(buf.String(), "\n")
191198
}
192199

193-
func (c *Command) ChecksumCalculator(workDir string) error {
200+
func (c *Command) ChecksumCalculator(shell, workDir string) error {
201+
if c.ChecksumCmd != "" {
202+
checksumResult, err := checksum.CalculateChecksumFromCmd(shell, workDir, c.ChecksumCmd)
203+
if err != nil {
204+
return err
205+
}
206+
c.ChecksumMap = make(map[string]string, 1)
207+
c.ChecksumMap[checksum.DefaultChecksumKey] = checksumResult
208+
return nil
209+
}
210+
194211
if len(c.ChecksumSources) == 0 {
195212
return nil
196213
}

config/config/config_test.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,26 @@ func TestParseConfig(t *testing.T) {
3737
}
3838
})
3939

40+
t.Run("allow persist checksum with checksum cmd", func(t *testing.T) {
41+
text := dedent.Dedent(`
42+
shell: bash
43+
commands:
44+
checksum-cmd:
45+
persist_checksum: true
46+
checksum_cmd: echo checksum
47+
cmd: echo ok
48+
`)
49+
50+
cfg := ConfigFixture(t, text)
51+
cmd := cfg.Commands["checksum-cmd"]
52+
53+
if !cmd.PersistChecksum {
54+
t.Fatalf("expected persist_checksum to be enabled")
55+
}
56+
57+
if cmd.ChecksumCmd != "echo checksum" {
58+
t.Fatalf("wrong checksum_cmd. Expect: %s, got: %s", "echo checksum", cmd.ChecksumCmd)
59+
}
60+
})
61+
4062
}

docs/docs/changelog.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ title: Changelog
55

66
## [Unreleased](https://github.com/lets-cli/lets/releases/tag/v0.0.X)
77

8+
* `[Added]` Add `checksum_cmd` directive for command-level checksum calculation from shell output.
9+
810
## [0.0.52](https://github.com/lets-cli/lets/releases/tag/v0.0.52)
911

1012
* `[Dependency]` update and pin goreleaser

docs/docs/config.md

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ title: Config reference
1919
* [env](#env)
2020
* [eval_env](#eval_env)
2121
* [checksum](#checksum)
22+
* [checksum_cmd](#checksum_cmd)
2223
* [persist_checksum](#persist_checksum)
2324
* [ref](#ref)
2425
* [args](#args)
@@ -734,6 +735,31 @@ commands:
734735
docker run --rm myrepo/app${LETS_CHECKSUM} python -m app
735736
```
736737

738+
### `checksum_cmd`
739+
740+
`key: checksum_cmd`
741+
742+
`type: string`
743+
744+
Use `checksum_cmd` when checksum should come from a shell command instead of a list of files.
745+
746+
The command runs with the same effective `shell` and `work_dir` as the command itself, so command-level overrides apply here too.
747+
748+
Result then can be accessed via `LETS_CHECKSUM` env variable.
749+
750+
Example:
751+
752+
```yaml
753+
shell: sh
754+
755+
commands:
756+
build-image:
757+
shell: bash
758+
work_dir: backend
759+
checksum_cmd: |
760+
[[ -f package-lock.json ]] && sha1sum package-lock.json | cut -d' ' -f1
761+
cmd: docker build -t myrepo/app:${LETS_CHECKSUM} .
762+
```
737763

738764
### `persist_checksum`
739765

@@ -743,7 +769,7 @@ commands:
743769

744770
This feature is useful when you want to know that something has changed between two executions of a command.
745771

746-
`persist_checksum` can be used only if `checksum` declared for command.
772+
`persist_checksum` can be used only if `checksum` or `checksum_cmd` declared for command.
747773

748774
If set to `true`, each run all calculated checksums will be stored to disk.
749775

@@ -768,6 +794,19 @@ commands:
768794
- Readme.md
769795
```
770796

797+
`checksum_cmd` can be persisted too:
798+
799+
```yaml
800+
commands:
801+
build-image:
802+
persist_checksum: true
803+
checksum_cmd: git rev-parse HEAD
804+
cmd: |
805+
if [[ ${LETS_CHECKSUM_CHANGED} == true ]]; then
806+
docker build -t myrepo/app:${LETS_CHECKSUM} .
807+
fi
808+
```
809+
771810
Resulting env will be:
772811

773812
* `LETS_CHECKSUM_DEPS` - checksum of deps files
@@ -827,4 +866,3 @@ commands:
827866
**`Experimental feature`**
828867

829868
`args` is used only with [ref](#ref) and allows to set additional positional args to referenced command. See [ref](#ref) example.
830-

executor/executor.go

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,22 @@ func formatOptsUsageError(err error, opts docopt.Opts, cmdName string, rawOption
151151
return fmt.Errorf("%s\n\n%s", errTpl, rawOptions)
152152
}
153153

154+
func (e *Executor) shellForCommand(command *config.Command) string {
155+
if command.Shell != "" {
156+
return command.Shell
157+
}
158+
159+
return e.cfg.Shell
160+
}
161+
162+
func (e *Executor) workDirForCommand(command *config.Command) string {
163+
if command.WorkDir != "" {
164+
return command.WorkDir
165+
}
166+
167+
return e.cfg.WorkDir
168+
}
169+
154170
// Init Command before execution:
155171
// - parse docopt
156172
// - calculate checksum.
@@ -173,7 +189,7 @@ func (e *Executor) initCmd(ctx *Context) error {
173189
}
174190

175191
// calculate checksum if needed
176-
if err := cmd.ChecksumCalculator(e.cfg.WorkDir); err != nil {
192+
if err := cmd.ChecksumCalculator(e.shellForCommand(cmd), e.workDirForCommand(cmd)); err != nil {
177193
return fmt.Errorf("failed to calculate checksum for command '%s': %w", cmd.Name, err)
178194
}
179195

@@ -255,10 +271,7 @@ func (e *Executor) setupEnv(osCmd *exec.Cmd, command *config.Command, shell stri
255271
// Passing ctx will change behavior of program drastically - it will kill process if context will be canceled.
256272
func (e *Executor) newOsCommand(command *config.Command, cmdScript string) (*exec.Cmd, error) {
257273
script := joinBeforeAndScript(e.cfg.Before, cmdScript)
258-
shell := e.cfg.Shell
259-
if command.Shell != "" {
260-
shell = command.Shell
261-
}
274+
shell := e.shellForCommand(command)
262275

263276
args := []string{"-c", script}
264277
if len(command.Args) > 0 {
@@ -277,10 +290,7 @@ func (e *Executor) newOsCommand(command *config.Command, cmdScript string) (*exe
277290
osCmd.Stdin = os.Stdin
278291

279292
// set working directory for command
280-
osCmd.Dir = e.cfg.WorkDir
281-
if command.WorkDir != "" {
282-
osCmd.Dir = command.WorkDir
283-
}
293+
osCmd.Dir = e.workDirForCommand(command)
284294

285295
if err := e.setupEnv(osCmd, command, shell); err != nil {
286296
return nil, err

executor/executor_test.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package executor
2+
3+
import (
4+
"context"
5+
"os"
6+
"path/filepath"
7+
"testing"
8+
9+
"github.com/lets-cli/lets/checksum"
10+
"github.com/lets-cli/lets/config/config"
11+
)
12+
13+
func TestInitCmdUsesCommandShellAndWorkDirForChecksum(t *testing.T) {
14+
tempDir := t.TempDir()
15+
projectDir := filepath.Join(tempDir, "project")
16+
17+
if err := os.Mkdir(projectDir, 0o755); err != nil {
18+
t.Fatalf("can not create project dir: %s", err)
19+
}
20+
21+
if err := os.WriteFile(filepath.Join(projectDir, "checksum.txt"), []byte("command-checksum"), 0o600); err != nil {
22+
t.Fatalf("can not write checksum file: %s", err)
23+
}
24+
25+
cfg := &config.Config{
26+
Shell: "sh",
27+
WorkDir: tempDir,
28+
}
29+
cmd := &config.Command{
30+
Name: "checksum-cmd",
31+
Shell: "bash",
32+
WorkDir: projectDir,
33+
SkipDocopts: true,
34+
ChecksumCmd: "[[ -f checksum.txt ]] && cat checksum.txt",
35+
}
36+
37+
executor := NewExecutor(cfg, nil)
38+
ctx := NewExecutorCtx(context.Background(), cmd)
39+
40+
if err := executor.initCmd(ctx); err != nil {
41+
t.Fatalf("initCmd failed: %s", err)
42+
}
43+
44+
if got := cmd.ChecksumMap[checksum.DefaultChecksumKey]; got != "command-checksum" {
45+
t.Fatalf("wrong checksum output. Expect: %s, got: %s", "command-checksum", got)
46+
}
47+
}

lets.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,3 +115,7 @@ commands:
115115
run-docs:
116116
work_dir: docs
117117
cmd: npm start
118+
119+
x:
120+
checksum_cmd: echo xxx_checksum
121+
cmd: echo checksum for x is ${LETS_CHECKSUM}

0 commit comments

Comments
 (0)