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
3 changes: 2 additions & 1 deletion .github/workflows/test-suite.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ jobs:
os: ubuntu-24.04-arm
- name: 🍎 MacOS (amd64)
os: macos-13
continue-on-error: true
- name: 🍎 MacOS (arm64)
os: macos-latest
needs-sidecar: true
Expand All @@ -76,6 +77,7 @@ jobs:
- name: 🖥️ Windows (arm64)
os: windows-11-arm
needs-sidecar: true
continue-on-error: true
runs-on: ${{ matrix.os }}
needs: [ client-ssh-key ]
continue-on-error: ${{ matrix.continue-on-error || false }}
Expand Down Expand Up @@ -139,7 +141,6 @@ jobs:
name: test-log-${{ matrix.os }}
path: .gotmp/gotest.log
retention-days: 3
if-no-files-found: error

lint:
name: 🧹 Lint & Code Quality
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ LOCAL_BIN:=$(CURDIR)/bin

# Linter config.
GOLANGCI_BIN:=$(LOCAL_BIN)/golangci-lint
GOLANGCI_TAG:=2.3.0
GOLANGCI_TAG:=2.5.0

GOTESTFMT_BIN:=$(GOBIN)/gotestfmt

Expand Down
84 changes: 23 additions & 61 deletions app.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,10 @@ package launchr

import (
"errors"
"fmt"
"io"
"os"
"reflect"

"github.com/launchrctl/launchr/internal/launchr"
"github.com/launchrctl/launchr/pkg/action"
_ "github.com/launchrctl/launchr/plugins" // include default plugins
)

Expand All @@ -20,11 +17,11 @@ type appImpl struct {
// FS related.
mFS []ManagedFS
workDir string
cfgDir string

// Services.
streams Streams
services map[ServiceInfo]Service
mask *SensitiveMask
services *ServiceManager
pluginMngr PluginManager
}

Expand All @@ -40,46 +37,20 @@ func (app *appImpl) SetStreams(s Streams) { app.streams = s }
func (app *appImpl) RegisterFS(fs ManagedFS) { app.mFS = append(app.mFS, fs) }
func (app *appImpl) GetRegisteredFS() []ManagedFS { return app.mFS }

func (app *appImpl) SensitiveWriter(w io.Writer) io.Writer {
return NewMaskingWriter(w, app.SensitiveMask())
}
func (app *appImpl) SensitiveMask() *SensitiveMask { return launchr.GlobalSensitiveMask() }
func (app *appImpl) SensitiveWriter(w io.Writer) io.Writer { return app.SensitiveMask().MaskWriter(w) }
func (app *appImpl) SensitiveMask() *SensitiveMask { return app.mask }

func (app *appImpl) RootCmd() *Command { return app.cmd }
func (app *appImpl) CmdEarlyParsed() launchr.CmdEarlyParsed { return app.earlyCmd }

func (app *appImpl) Services() *ServiceManager { return app.services }

func (app *appImpl) AddService(s Service) {
info := s.ServiceInfo()
launchr.InitServiceInfo(&info, s)
if _, ok := app.services[info]; ok {
panic(fmt.Errorf("service %s already exists, review your code", info))
}
app.services[info] = s
app.services.Add(s)
}

func (app *appImpl) GetService(v any) {
// Check v is a pointer and implements [Service] to set a value later.
t := reflect.TypeOf(v)
isPtr := t != nil && t.Kind() == reflect.Pointer
var stype reflect.Type
if isPtr {
stype = t.Elem()
}

// v must be [Service] but can't equal it because all elements implement it
// and the first value will always be returned.
intService := reflect.TypeOf((*Service)(nil)).Elem()
if !isPtr || !stype.Implements(intService) || stype == intService {
panic(fmt.Errorf("argument must be a pointer to a type (interface) implementing Service, %q given", t))
}
for _, srv := range app.services {
st := reflect.TypeOf(srv)
if st.AssignableTo(stype) {
reflect.ValueOf(v).Elem().Set(reflect.ValueOf(srv))
return
}
}
panic(fmt.Sprintf("service %q does not exist", stype))
app.services.Get(v)
}

// init initializes application and plugins.
Expand Down Expand Up @@ -108,34 +79,25 @@ func (app *appImpl) init() error {
}
app.cmd.SetVersionTemplate(`{{ appVersionFull }}`)
app.earlyCmd = launchr.EarlyPeekCommand()
// Set io streams.
app.SetStreams(MaskedStdStreams(app.SensitiveMask()))
app.cmd.SetIn(app.streams.In().Reader())
app.cmd.SetOut(app.streams.Out())
app.cmd.SetErr(app.streams.Err())
app.mask = launchr.NewSensitiveMask("****")

// Set working dir and config dir.
app.cfgDir = "." + name
app.workDir = launchr.MustAbs(".")
actionsPath := launchr.MustAbs(EnvVarActionsPath.Get())
// Initialize managed FS for action discovery.
// Initialize managed FS for action discovery and set working dir
app.workDir = MustAbs(".")
app.mFS = make([]ManagedFS, 0, 4)
app.RegisterFS(action.NewDiscoveryFS(os.DirFS(actionsPath), app.GetWD()))

// Prepare dependencies.
app.services = make(map[ServiceInfo]Service)
app.services = launchr.NewServiceManager()
app.pluginMngr = launchr.NewPluginManagerWithRegistered()
// @todo consider home dir for global config.
config := launchr.ConfigFromFS(os.DirFS(app.cfgDir))
actionMngr := action.NewManager(
action.WithDefaultRuntime(config),
action.WithContainerRuntimeConfig(config, name+"_"),
)

// Register services for other modules.
app.AddService(actionMngr)
app.AddService(app.pluginMngr)
app.AddService(config)

// Register svcMngr for other modules.
app.services.Add(app.mask)
app.services.Add(app.pluginMngr)

// Set io streams.
app.SetStreams(MaskedStdStreams(app.mask))
app.cmd.SetIn(app.streams.In().Reader())
app.cmd.SetOut(app.streams.Out())
app.cmd.SetErr(app.streams.Err())

Log().Debug("initialising application")

Expand All @@ -148,7 +110,7 @@ func (app *appImpl) init() error {
return err
}
}
Log().Debug("init success", "wd", app.workDir, "actions_dir", actionsPath)
Log().Debug("init success", "wd", app.workDir)

return nil
}
Expand Down
2 changes: 1 addition & 1 deletion docs/actions.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ runtime:
command:
- python3
- {{ .myArg1 }} {{ .myArg2 }}
- {{ .optStr }}
- '{{ .optStr }}'
- ${ENV_VAR}
```

Expand Down
70 changes: 24 additions & 46 deletions docs/actions.schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,8 @@ The action provides basic templating for all file based on arguments, options an
For templating, the standard Go templating engine is used.
Refer to [the library documentation](https://pkg.go.dev/text/template) for usage examples.

Since `action.yaml` must be valid YAML, wrap template strings in quotes to ensure proper parsing.

Arguments and Options are available by their machine names - `{{ .myArg1 }}`, `{{ .optStr }}`, `{{ .optArr }}`, etc.

### Predefined variables:
Expand Down Expand Up @@ -227,85 +229,61 @@ runtime:
image: {{ .optStr }}:latest
command:
- {{ .myArg1 }} {{ .MyArg2 }}
- {{ .optBool }}
- '{{ .optBool }}'
```

### Available Command Template Functions

### `removeLine`
**Description:** A special template directive that removes the entire line from the final output.

**Usage:**

``` yaml
- "{{ if condition }}value{{ else }}{{ removeLine }}{{ end }}"
```

### `isNil`

**Description:** Checks if a value is nil.

**Usage:**

```yaml
- "{{ if not (isNil .param_name) }}--param={{ .param_name }}{{ else }}{{ removeLine }}{{ end }}"
```gotemplate
{{ if not (isNil .param_name) }}--param={{ .param_name }}{{ end }}
```

### `isSet`

**Description:** Checks if a value has been set (opposite of `isNil`).

```yaml
- "{{ if isSet .param_name }}--param={{ .param_name }}{{else}}{{ removeLine }}{{ end }}"
```gotemplate
{{ if isSet .param_name }}--param={{ .param_name }}{{ end }}
```

### `isChanged`

**Description:** Checks if an option or argument value has been changed (dirty).
**Description:** Checks if an option or argument value has been changed (input by user).

**Usage:**

```yaml
- '{{ if isChanged "param_name"}}--param={{.param_name}}{{else}}{{ removeLine }}{{ end }}'
```gotemplate
{{ if isChanged "param_name"}}--param={{.param_name}}{{ end }}
```

### `removeLineIfNil`
**Description:** Removes the entire command line if the value is nil.
### `default`

**Usage:**

```yaml
- "{{ removeLineIfNil .param_name }}"
```

### `removeLineIfSet`
**Description:** Removes the entire command line if the value is set (has no nil value).
**Description:** Returns a default value when the first parameter is `nil` or empty.
Emptiness is determined by its zero value - empty string `""`, integer `0`, structs with all zero-value fields, etc.
Or type implements `interface { IsEmpty() bool }`.

**Usage:**

```yaml
- "{{ removeLineIfSet .param_name }}"
```gotemplate
{{ .nil_value | default "foo" }}
{{ default .nil_value "bar" }}
```

### `removeLineIfChanged`
### `config.Get`

**Description:** Removes the command line entry if the option/argument value has changed.
**Description:** Returns a [config](config.md) value by a path.

**Usage:**

``` yaml
- '{{ removeLineIfChanged "param_name" }}'
```

### `removeLineIfNotChanged`

**Description:** Removes the command line entry if the option/argument value has not changed by the user.
Opposite of `removeLineIfChanged`

**Usage:**

``` yaml
- '{{ removeLineIfNotChanged "param_name" }}'
```gotemplate
{{ config "foo.bar" }} # retrieves value of any type
{{ index (config "foo.array-elem") 1 }} # retrieves specific array element
{{ config "foo.null-elem" | default "foo" }} # uses default if value is nil
{{ config "foo.missing-elem" | default "bar" }} # uses default if key doesn't exist
```


Expand Down
2 changes: 1 addition & 1 deletion docs/development/plugin.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ Add the processor to the Action Manager:

```go
var am action.Manager
app.GetService(&am)
app.Services().Get(&am)

procReplace := GenericValueProcessor[procTestReplaceOptions]{
Types: []jsonschema.Type{jsonschema.String},
Expand Down
6 changes: 3 additions & 3 deletions docs/development/service.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import (
// Get a service from the App.
func (p *Plugin) OnAppInit(app launchr.App) error {
var cfg launchr.Config
app.GetService(&cfg) // Pass a pointer to init the value.
app.Services().Get(&cfg) // Pass a pointer to init the value.
return nil
}
```
Expand All @@ -41,7 +41,7 @@ import (
)

// Define a service and implement service interface.
// It is important to have a unique interface, the service is identified by it in launchr.GetService().
// It is important to have a unique interface, the service is identified by it in [launchr.ServiceManager].Get().
type ExampleService interface {
launchr.Service // Inherit launchr.Service
// Provide other methods if needed.
Expand All @@ -58,7 +58,7 @@ func (ex *exampleSrvImpl) ServiceInfo() launchr.ServiceInfo {
// Register a service inside launchr.
func (p *Plugin) OnAppInit(app launchr.App) error {
srv := &exampleSrvImpl{}
app.AddService(srv)
app.Services().Add(srv)
return nil
}
```
2 changes: 1 addition & 1 deletion docs/development/test.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ concurrency:
jobs:
tests:
name: 🛡️ Multi-Platform Testing Suite
uses: launchr/launchr/.github/workflows/test-suite.yaml@main
uses: launchrctl/launchr/.github/workflows/test-suite.yaml@main
```

### Benefits of Reusable Workflows
Expand Down
10 changes: 3 additions & 7 deletions example/actions/arguments/action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,7 @@ runtime:
- /action/main.sh
- "{{ .arg1 }}"
- "{{ .arg2 }}"
- "{{ .firstoption|removeLineIfNil }}"
- "{{ if not (isNil .secondoption) }}--secondoption={{ .secondoption }}{{ else }}{{ removeLine }}{{ end }}"
- "{{ if not (isNil .secondoption) }}--secondoption={{ .secondoption }}{{else}}Second option is nil{{ end }}"
- "{{ if isSet .thirdoption }}--thirdoption={{ .thirdoption }}{{else}}Third option is not set{{ end }}"
- "{{ removeLineIfSet .thirdoption }}"
- '{{ if not (isChanged "thirdoption")}}Third Option is not Changed{{else}}{{ removeLine }}{{ end }}'
- '{{ removeLineIfChanged "thirdoption" }}'
- '{{ if isChanged "thirdoption"}}Third Option is Changed{{else}}{{ removeLine }}{{ end }}'
- '{{ removeLineIfNotChanged "thirdoption" }}'
- '{{ if not (isChanged "thirdoption")}}Third Option is not Changed{{ end }}'
- '{{ if isChanged "thirdoption"}}Third Option is Changed{{ end }}'
16 changes: 12 additions & 4 deletions example/actions/platform/actions/build/action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,16 @@ action:

runtime:
type: container
# image: python:3.7-slim
image: ubuntu
# command: python3 {{ .opt4 }}
command: ["sh", "-c", "for i in $(seq 60); do if [ $((i % 2)) -eq 0 ]; then echo \"stdout: $$i\"; else echo \"stderr: $$i\" >&2; fi; sleep 1; done"]
# command: /bin/bash
command:
- "sh"
- "-c"
- |
for i in $(seq 60); do
if [ $((i % 2)) -eq 0 ]; then
echo "stdout: $$i";
else
echo "stderr: $$i" >&2;
fi;
sleep 1;
done
2 changes: 1 addition & 1 deletion gen.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ func (app *appImpl) gen() error {

// Do not fail if some flags change in future builds.
flags := app.cmd.Flags()
flags.ParseErrorsWhitelist.UnknownFlags = true
flags.ParseErrorsAllowlist.UnknownFlags = true
// Working directory flag is helpful because "go run" can't be run outside
// a project directory and use all its go.mod dependencies.
flags.StringVar(&config.WorkDir, "work-dir", config.WorkDir, "Working directory")
Expand Down
Loading
Loading