Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add a new .taskrc.yml to enable experiments #1982

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 8 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
56 changes: 50 additions & 6 deletions internal/experiments/experiments.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import (
"slices"
"strings"

"gopkg.in/yaml.v3"

"github.com/Ladicle/tabwriter"
"github.com/joho/godotenv"
"github.com/spf13/pflag"
Expand All @@ -17,6 +19,15 @@ import (

const envPrefix = "TASK_X_"

var defaultConfigFilenames = []string{
".taskrc.yml",
".taskrc.yaml",
}

type ExperimentConfigFile struct {
vmaerten marked this conversation as resolved.
Show resolved Hide resolved
Experiments map[string]string `yaml:"experiments"`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: Currently the only acceptable values for experiments are integers (1, 2). However, technically the value is saved as a string (mostly because env vars are always strings). If we think that we will never use anything other than integers, then maybe the values in the config file should be ints:

Suggested change
Experiments map[string]string `yaml:"experiments"`
Experiments map[string]int `yaml:"experiments"`

Open to ideas about why we'd want to support strings though.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pd93 Good question!

Since experiment values are typed as strings (because environment variables are strings), I considered two options:

  1. Define Experiments map[string]string with yaml:"experiments" as map[string]string, allowing the parser to handle the conversion. This approach lets me keep the existing code.
  2. Define Experiments as map[string]int and modify the code to convert all environment variables to integers.

I choose the first one to keep the existing code but you may be right, we could / should define that experiment should be only int

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've converted everything to int :)

}

type Experiment struct {
Name string
Enabled bool
Expand All @@ -32,8 +43,11 @@ var (
EnvPrecedence Experiment
)

var experimentConfig ExperimentConfigFile

func init() {
readDotEnv()
experimentConfig = readConfig()
GentleForce = New("GENTLE_FORCE")
RemoteTaskfiles = New("REMOTE_TASKFILES")
AnyVariables = New("ANY_VARIABLES", "1", "2")
Expand All @@ -45,7 +59,13 @@ func New(xName string, enabledValues ...string) Experiment {
if len(enabledValues) == 0 {
enabledValues = []string{"1"}
}
value := getEnv(xName)

value := experimentConfig.Experiments[xName]

if value == "" {
value = getEnv(xName)
}

return Experiment{
Name: xName,
Enabled: slices.Contains(enabledValues, value),
Expand All @@ -65,7 +85,7 @@ func getEnv(xName string) string {
return os.Getenv(envName)
}

func getEnvFilePath() string {
func getFilePath(filename string) string {
// Parse the CLI flags again to get the directory/taskfile being run
// We use a flagset here so that we can parse a subset of flags without exiting on error.
var dir, taskfile string
Expand All @@ -76,18 +96,18 @@ func getEnvFilePath() string {
_ = fs.Parse(os.Args[1:])
// If the directory is set, find a .env file in that directory.
if dir != "" {
return filepath.Join(dir, ".env")
return filepath.Join(dir, filename)
}
// If the taskfile is set, find a .env file in the directory containing the Taskfile.
if taskfile != "" {
return filepath.Join(filepath.Dir(taskfile), ".env")
return filepath.Join(filepath.Dir(taskfile), filename)
}
// Otherwise just use the current working directory.
return ".env"
return filename
}

func readDotEnv() {
env, _ := godotenv.Read(getEnvFilePath())
env, _ := godotenv.Read(getFilePath(".env"))
// If the env var is an experiment, set it.
for key, value := range env {
if strings.HasPrefix(key, envPrefix) {
Expand All @@ -96,6 +116,30 @@ func readDotEnv() {
}
}

func readConfig() ExperimentConfigFile {
var cfg ExperimentConfigFile

var content []byte
var err error
for _, filename := range defaultConfigFilenames {
path := getFilePath(filename)
content, err = os.ReadFile(path)
if err == nil {
break
}
}

if err != nil {
return ExperimentConfigFile{}
}

if err := yaml.Unmarshal(content, &cfg); err != nil {
return ExperimentConfigFile{}
}

return cfg
}

func printExperiment(w io.Writer, l *logger.Logger, x Experiment) {
l.FOutf(w, logger.Yellow, "* ")
l.FOutf(w, logger.Green, x.Name)
Expand Down
41 changes: 31 additions & 10 deletions website/docs/experiments/experiments.mdx
vmaerten marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ slug: /experiments/
sidebar_position: 6
---

import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';

# Experiments

:::caution
Expand Down Expand Up @@ -39,23 +42,41 @@ Which method you use depends on how you intend to use the experiment:
1. Prefixing your task commands with the relevant environment variable(s). For
example, `TASK_X_{FEATURE}=1 task {my-task}`. This is intended for one-off
invocations of Task to test out experimental features.
1. Adding the relevant environment variable(s) in your "dotfiles" (e.g.
2. Adding the relevant environment variable(s) in your "dotfiles" (e.g.
vmaerten marked this conversation as resolved.
Show resolved Hide resolved
`.bashrc`, `.zshrc` etc.). This will permanently enable experimental features
for your personal environment.

```shell title="~/.bashrc"
export TASK_X_FEATURE=1
```

1. Creating a `.env` file in the same directory as your root Taskfile that
contains the relevant environment variable(s). This allows you to enable an
experimental feature at a project level. If you commit the `.env` file to
source control then other users of your project will also have these
experiments enabled.

```shell title=".env"
TASK_X_FEATURE=1
```
3. Creating a `.env` or a `.task-experiments.yml` file in the same directory as
your root Taskfile.\
The `.env` file should contain the relevant environment
variable(s), while the `.task-experiments.yml` file should use a YAML format
where each experiment is defined as a key with a corresponding value.

This allows you to enable an experimental feature at a project level. If you
commit this file to source control, then other users of your project will
also have these experiments enabled.

If both files are present, the values in the `.task-experiments.yml` file
will take precedence.

<Tabs values={[ {label: '.task-experiments.yml', value: 'yaml'}, {label: '.env', value: 'env'}]}>
<TabItem value="yaml">
```yaml title=".taskrc.yml"
experiments:
FEATURE: 1
```
</TabItem>

<TabItem value="env">
```shell title=".env"
TASK_X_FEATURE=1
```
</TabItem>
</Tabs>

## Workflow

Expand Down
15 changes: 15 additions & 0 deletions website/static/schema-taskrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"title": "Taskrc YAML Schema",
"description": "Schema for .taskrc files.",
"type": "object",
"properties": {
"experiments": {
"type": "object",
"additionalProperties": {
"type": "integer"
}
}
},
"additionalProperties": false
}
Loading