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
8 changes: 4 additions & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
FROM golang:1.24-bookworm as builder
FROM golang:1.24-bookworm AS builder

ENV GOPROXY https://proxy.golang.org
ENV CGO_ENABLED 1
ENV GOPROXY=https://proxy.golang.org
ENV CGO_ENABLED=1
# disable all compiler errors
ENV CGO_CFLAGS=-w

Expand All @@ -26,6 +26,6 @@ COPY go.sum .

RUN go mod download

FROM golangci/golangci-lint:v1.64.7-alpine as linter
FROM golangci/golangci-lint:v1.64.7-alpine AS linter

RUN mkdir -p /.cache && chmod -R 777 /.cache
24 changes: 24 additions & 0 deletions config/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,22 @@ import (
"strings"

"github.com/lets-cli/lets/config/path"
"github.com/lets-cli/lets/set"
"github.com/lets-cli/lets/util"
"gopkg.in/yaml.v3"
)

var keywords = set.NewSet[string](
"version",
"shell",
"env",
"eval_env",
"init",
"before",
"mixins",
"commands",
)

// Config is a struct for loaded config file.
type Config struct {
// absolute path to work dir - where config is placed
Expand All @@ -37,6 +49,18 @@ type Config struct {
}

func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error {
var raw map[string]interface{}
if err := unmarshal(&raw); err != nil {
return err
}

// check if config has unsupported keywords
for key := range raw {
if !keywords.Contains(key) && !strings.HasPrefix(key, "x-") {
return fmt.Errorf("keyword '%s' not supported", key)
}
}

var config struct {
Version Version
Mixins []*Mixin
Expand Down
51 changes: 51 additions & 0 deletions config/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package config

import (
"bytes"
"maps"
"testing"

"github.com/lithammer/dedent"
Expand Down Expand Up @@ -37,4 +38,54 @@ func TestParseConfig(t *testing.T) {
}
})

t.Run("parse env with alias", func(t *testing.T) {
text := dedent.Dedent(`
shell: bash

x-default-env: &default-env
HELLO: WORLD

env:
<<: *default-env
FOO: BAR

commands:
hello:
cmd: [echo, Hello]
`)
cfg := ConfigFixture(t, text)

env := cfg.Env.Dump()
expected := map[string]string{
"FOO": "BAR",
"HELLO": "WORLD",
}
if !maps.Equal(env, expected) {
t.Errorf("wrong output. \nexpect %s \ngot: %s", expected, env)
}
})

t.Run("invalid alias name - does not start with x-", func(t *testing.T) {
text := dedent.Dedent(`
shell: bash

default-env: &default-env
HELLO: WORLD

env:
<<: *default-env
FOO: BAR

commands:
hello:
cmd: [echo, Hello]
`)

buf := bytes.NewBufferString(text)
c := NewConfig(".", ".", ".")
err := yaml.NewDecoder(buf).Decode(&c)
if err.Error() != "keyword 'default-env' not supported" {
t.Errorf("config must not allow custom keywords")
}
})
}
11 changes: 11 additions & 0 deletions config/config/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,17 @@ func (e *Envs) UnmarshalYAML(node *yaml.Node) error {
keyNode := node.Content[i]
valueNode := node.Content[i+1]

// handle <<: *aliased case
if keyNode.Tag == "!!merge" {
aliasedEnv := &Envs{}
err := aliasedEnv.UnmarshalYAML(valueNode.Alias)
if err != nil {
return errors.New("lets: can not parse aliased env")
}
e.Merge(aliasedEnv)
continue
}

envAsStr := ""

if err := valueNode.Decode(&envAsStr); err == nil {
Expand Down
27 changes: 26 additions & 1 deletion docs/docs/best_practices.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ title: Best practices

### Naming conventions

Prefer single word over plural.
Prefer single word over plural.

It is better to leverage semantics of `lets` as an intention to do something. For example it is natural saying `lets test` or `lets build` something.

Expand Down Expand Up @@ -106,3 +106,28 @@ As you can see, we execute `build` command each time we execute `run` command (`

`persist_checksum` will save calculated checksum to `.lets` directory and all subsequent calls of `build` command will
read checksum from disk, calculate new checksum, and compare them. If `package.json` will change - we will rebuild the image.


### Initialize project using `init`

You can use `init` keyword to write a script that will do some initialization on lets startup, like creating some dirs, configs or installing project dependencies.

By default, `init` runs each time the `lets` program is executed.

You can make `init` conditional, by simply creating a file and checking if it exists at the start of `init` script.

Example:

```
shell: bash

init: |
if [[ ! -f .lets/init_done ]]; then
echo "calling init script"
touch .lets/init_done
fi
```

In this example we are checking for `.lets/init_done` file existence. If it does not exist, we will call init script and create `init_done` file as a marker of successfull init script invocation.

We are using `.lets` dir here because this dir will be created by `lets` itself and is generally a good place to create such files, but you are free to create files with any name and in any directory you want.
3 changes: 3 additions & 0 deletions docs/docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ title: Changelog
## [Unreleased](https://github.com/lets-cli/lets/releases/tag/v0.0.X)

* `[Dependency]` update go to `1.24`
* `[Added]` support custom top-level keywords that start with `x-`
* `[Added]` check for invalid top-level keywords during config parsing
* `[Added]` support YAML aliases in `env` - env will be merged aliases mapping

## [0.0.55](https://github.com/lets-cli/lets/releases/tag/v0.0.55)

Expand Down
107 changes: 85 additions & 22 deletions docs/docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,36 @@ id: config
title: Config reference
---

* [shell](#shell)
* [mixins](#mixins)
* [env](#global-env)
* [eval_env](#global-eval_env)
* [init](#global-init)
* [before](#global-before)
* [commands](#commands)
* [description](#description)
* [cmd](#cmd)
* [work_dir](#work_dir)
* [after](#after)
* [depends](#depends)
* [options](#options)
* [env](#env)
* [eval_env](#eval_env)
* [checksum](#checksum)
* [persist_checksum](#persist_checksum)
* [ref](#ref)
* [args](#args)
- [Top-level directives:](#top-level-directives)
- [Version](#version)
- [Shell](#shell)
- [Global env](#global-env)
- [Global eval\_env](#global-eval_env)
- [Global before](#global-before)
- [Global init](#global-init)
- [Conditional init](#conditional-init)
- [Mixins](#mixins)
- [Ignored mixins](#ignored-mixins)
- [Remote mixins `(experimental)`](#remote-mixins-experimental)
- [Commands](#commands)
- [Command directives:](#command-directives)
- [Short syntax](#short-syntax)
- [`cmd`](#cmd)
- [`description`](#description)
- [`work_dir`](#work_dir)
- [`shell`](#shell-1)
- [`after`](#after)
- [`depends`](#depends)
- [Override arguments in depends command](#override-arguments-in-depends-command)
- [`options`](#options)
- [`env`](#env)
- [`eval_env`](#eval_env)
- [`checksum`](#checksum)
- [`persist_checksum`](#persist_checksum)
- [`ref`](#ref)
- [`args`](#args)
- [Aliasing:](#aliasing)
- [Env aliasing](#env-aliasing)


## Top-level directives:
Expand Down Expand Up @@ -139,11 +150,19 @@ commands:

`type: string`

Specify init script which will be executed only once. It is execured right before first command call.
Specify init script which will be executed only once during each lets invocation. It is execured right before first command call.

`init` script is a good place for some intialization that sould be done once, for example,
> Main difference from `before` is that `before` called before each command invocation (including commands specified in depends)

create docker network, check if some directory exist, clear caches, etc.
`init` script is a good place for some initialization that should be done once at lets startup, for example:

* create docker network
* check if some directory exist
* clear caches,
* install dependencies
* etc.

Example usage:

```yaml
shell: bash
Expand Down Expand Up @@ -171,6 +190,29 @@ From before
Bar
```

#### Conditional init

If you need to make sure that code in `init` is called once with some condition,
you can for example create a file at the end of `init` script and check if this
file exists at the beginning of `init` script.

Example:

```
shell: bash

init: |
if [[ ! -f .lets/init_done ]]; then
echo "calling init script"
touch .lets/init_done
fi
```

In this example we are checking for `.lets/init_done` file existence. If it does not exist, we will call init script and create `init_done` file as a marker of successfull init script invocation.

We are using `.lets` dir here because this dir will be created by `lets` itself and is generally a good place to create such files, but you are free to create files with any name and in any directory you want.


### Mixins

`key: mixins`
Expand Down Expand Up @@ -828,3 +870,24 @@ commands:

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


## Aliasing:

Lets supports YAML aliasing in various places in the config

### Env aliasing

You can define any mapping and alias it in `env` configuration:

```yaml
shell: bash

.default-env: &default-env
FOO: BAR

env:
<<: *default-env
HELLO: WORLD
```

This will merge `env` and `.default-env`. Any environment variables declarations after `<<: ` will override variables defined in aliased map.
Loading