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

No env var inference in strict mode. #4527

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion cli/internal/graph/graph.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ func (g *CompleteGraph) GetPackageTaskVisitor(
useOldTaskHashable := false
if taskEnvMode == util.Infer {
if taskDefinition.PassthroughEnv != nil {
taskEnvMode = util.Strict
taskEnvMode = util.StrictIncludeFrameworkVars
} else {
// If we're in infer mode we have just detected non-usage of strict env vars.
// Since we haven't stabilized this we don't want to break their cache.
Expand Down
4 changes: 3 additions & 1 deletion cli/internal/run/global_hash.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ func calculateGlobalHashFromHashableInputs(full GlobalHashableInputs) (string, e
// In infer mode, if there is any passThru config (even if it is an empty array)
// we'll hash the whole object, so we can detect changes to that config
// Further, resolve the envMode to the concrete value.
full.envMode = util.Strict
full.envMode = util.StrictIncludeFrameworkVars
return newGlobalHash(full)
}

Expand All @@ -102,6 +102,8 @@ func calculateGlobalHashFromHashableInputs(full GlobalHashableInputs) (string, e
// Remove the passthroughs from hash consideration if we're explicitly loose.
full.envVarPassthroughs = nil
return newGlobalHash(full)
case util.StrictIncludeFrameworkVars:
fallthrough
case util.Strict:
// Collapse `nil` and `[]` in strict mode.
if full.envVarPassthroughs == nil {
Expand Down
2 changes: 1 addition & 1 deletion cli/internal/run/real_run.go
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,7 @@ func (ec *execContext) exec(ctx gocontext.Context, packageTask *nodes.PackageTas
currentState := env.GetEnvMap()
passthroughEnv := env.EnvironmentVariableMap{}

if packageTask.EnvMode == util.Strict {
if packageTask.EnvMode.IsStrict() {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

There are now two strict modes with slightly different meanings.

defaultPassthrough := []string{
"PATH",
"SHELL",
Expand Down
2 changes: 1 addition & 1 deletion cli/internal/run/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,7 @@ func (r *run) run(ctx gocontext.Context, targets []string) error {

globalEnvMode := rs.Opts.runOpts.EnvMode
if globalEnvMode == util.Infer && turboJSON.GlobalPassthroughEnv != nil {
globalEnvMode = util.Strict
globalEnvMode = util.StrictIncludeFrameworkVars
}

// RunSummary contains information that is statically analyzable about
Expand Down
27 changes: 21 additions & 6 deletions cli/internal/taskhash/taskhash.go
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,8 @@ func calculateTaskHashFromHashable(full *taskHashable, useOldTaskHashable bool)
// Remove the passthroughs from hash consideration if we're explicitly loose.
full.passthroughEnv = nil
return fs.HashObject(full)
case util.StrictIncludeFrameworkVars:
fallthrough
case util.Strict:
// Collapse `nil` and `[]` in strict mode.
if full.passthroughEnv == nil {
Expand Down Expand Up @@ -379,17 +381,30 @@ func (th *Tracker) CalculateTaskHash(packageTask *nodes.PackageTask, dependencyS
}

var keyMatchers []string
framework := inference.InferFramework(packageTask.Pkg)
if framework != nil && framework.EnvMatcher != "" {
// log auto detected framework and env prefix
logger.Debug(fmt.Sprintf("auto detected framework for %s", packageTask.PackageName), "framework", framework.Slug, "env_prefix", framework.EnvMatcher)
keyMatchers = append(keyMatchers, framework.EnvMatcher)
var framework *inference.Framework
envVarContainingExcludePrefix := ""

// In Strict environment variable mode we don't do framework env var inference.
// You must instead:
// - Specify all environment variables that you want available in your build environment.
// - Specify whether you want those considered for the hash or merely passed through.
//
// This allows advanced users to improve both cache hit ratio _and_ correctness with
// the tradeoff of increased configuration verbosity.
if packageTask.EnvMode != util.Strict {
envVarContainingExcludePrefix = "TURBO_CI_VENDOR_ENV_KEY"
framework := inference.InferFramework(packageTask.Pkg)
if framework != nil && framework.EnvMatcher != "" {
// log auto detected framework and env prefix
logger.Debug(fmt.Sprintf("auto detected framework for %s", packageTask.PackageName), "framework", framework.Slug, "env_prefix", framework.EnvMatcher)
keyMatchers = append(keyMatchers, framework.EnvMatcher)
}
Comment on lines +384 to +401
Copy link
Contributor Author

Choose a reason for hiding this comment

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

In whitespace-ignoring review mode, you'll see that this is just an if statement, a delayed variable configuration, and a comment. It's the only material behavior change in this PR.

}

envVars, err := env.GetHashableEnvVars(
packageTask.TaskDefinition.EnvVarDependencies,
keyMatchers,
"TURBO_CI_VENDOR_ENV_KEY",
envVarContainingExcludePrefix,
Comment on lines 406 to +407
Copy link
Contributor Author

Choose a reason for hiding this comment

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

These remain unset in Strict mode, triggering the behavior change.

)
if err != nil {
return "", err
Expand Down
15 changes: 13 additions & 2 deletions cli/internal/util/run_opts.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,24 @@ const (
Infer EnvMode = "Infer"
// Loose - environment variables are unconstrained
Loose EnvMode = "Loose"
// StrictIncludeFrameworkVars - environment variables are limited.
// They include vars from detected frameworks.
StrictIncludeFrameworkVars EnvMode = "StrictIncludeFrameworkVars"
// Strict - environment variables are limited
Strict EnvMode = "Strict"
)

// MarshalText implements TextMarshaler for the struct.
func (s EnvMode) MarshalText() (text []byte, err error) {
return []byte(strings.ToLower(string(s))), nil
func (em EnvMode) MarshalText() (text []byte, err error) {
if em == StrictIncludeFrameworkVars {
return []byte("strict-include-framework-vars"), nil
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I could include some fancy kebab-case handling thing, write my own, or just hardcode the singular edge case. I chose the one that wasn't a waste of time and bytes.

There are tests that include this value, so it won't regress.

}
return []byte(strings.ToLower(string(em))), nil
}

// IsStrict collapses the two Strict variants.
func (em EnvMode) IsStrict() bool {
return em == Strict || em == StrictIncludeFrameworkVars
Comment on lines +28 to +30
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We sometimes need to check these together. This makes it less error-prone.

}

// RunOpts holds the options that control the execution of a turbo run
Expand Down
21 changes: 21 additions & 0 deletions crates/turborepo-lib/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ pub enum DryRunMode {
pub enum EnvMode {
Infer,
Loose,
StrictIncludeFrameworkVars,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The name here is a bikeshed. Note that the name introducing friction for using this is intentional.

Happy to entertain other options but this is already less aggressive than my original. 😅

Copy link
Contributor

Choose a reason for hiding this comment

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

I think we should go with something a bit shorter, at least for the user-facing portions. We're intending to communicate that the env vars available will match the set of env vars that turbo considers to be dependencies, and that means we're going infer some vars. Some ideas:

  • inferred
  • hashed
  • hashed-only
  • auto
  • detected

I'm least wild about the "hashed" variants since we don't want to force users to think about hashing. For better or worse they already need to think about inference at least a little, so I think that's less bad. "auto" and "detected" are pretty general. After writing this out, my vote goes to "auto". It suggests that turbo is doing something beyond what the user has specified, and I think it also contrasts nicely with "strict": "do your best guess at what will work" vs "don't do anything, I'm going to specify everything". I'm open to other [short] suggestions though.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

After implementation I don't think we can drop infer so we're going to be four states:

  • --infer: we figure it out per-task based on turbo.json. (collapses to loose or strict-include-framework-vars)
  • --loose: you get everything.
  • --strict: we include only what you tell us.
  • --strict-include-framework-vars: we include what you tell us and the framework vars.

This primary way to get this is: "you defined passThroughEnv but don't pass in a command line flag". This is what gets you strict-include-framework-vars. In general the user should rarely want or need to specify this.

The primary use case for manually specifying this flag is "I want strict mode for all my tasks, and for tasks that are Next.js I want to include NEXT_PUBLIC_*." Packages basically never change frameworks, so using this is really just a feature request for "globbing over env vars" in turbo.json."

The problem here is that "auto", "detect", and "infer" are all synonyms and are extremely difficult to differentiate so we need two names for things which will basically never be specified on the command line, one of which is the "wrong" solution to the problem:

  • "we pull it from turbo.json"
  • "strict + framework vars"

Copy link
Contributor Author

Choose a reason for hiding this comment

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

A lot of words to say:

  • I think the clarity of length is valuable. This length improves the run summary.
  • I believe that users will rarely, if ever, actually specify it.
  • The friction will push people toward doing it the way we want them to, which is more-easily shareable: in turbo.json, in env and passThroughEnv, and using a yet-unimplemented globbing feature.

Strict,
}

Expand Down Expand Up @@ -728,6 +729,26 @@ mod test {
"env_mode: specified strict"
);

assert_eq!(
Args::try_parse_from([
"turbo",
"run",
"build",
"--experimental-env-mode",
"strict-include-framework-vars"
])
.unwrap(),
Args {
command: Some(Command::Run(Box::new(RunArgs {
tasks: vec!["build".to_string()],
env_mode: EnvMode::StrictIncludeFrameworkVars,
..get_default_run_args()
}))),
..Args::default()
},
"env_mode: specified strict-include-framework-vars"
);

assert_eq!(
Args::try_parse_from(["turbo", "run", "build", "lint", "test"]).unwrap(),
Args {
Expand Down
4 changes: 4 additions & 0 deletions docs/pages/repo/docs/core-concepts/caching.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,10 @@ To alter the cache for _all_ tasks, you can declare environment variables in the

### Automatic environment variable inclusion

<Callout type="info">
Turborepo does automatic environment variable inclusion unless you pass `--env-mode=strict` in order to disable this behavior. In `strict` environment variable mode you must specify every environment variable you want available in your execution environment via `env` or `globalEnv` if you want them considered for the hash and `experimentalPassThroughEnv` or `experimentalGlobalPassThroughEnv` if not.
</Callout>

To help ensure correct caching across environments, Turborepo automatically infers and includes public environment variables when calculating cache keys for apps built with detected frameworks. You can safely omit framework-specific public environment variables from `turbo.json`:

```diff filename="turbo.json"
Expand Down
35 changes: 26 additions & 9 deletions docs/pages/repo/docs/reference/command-line-reference.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -188,20 +188,21 @@ Task details include:

Controls the available environment variables to your tasks.

| option | description |
| ------ | ---------------------------------------------------------- |
| infer | infers strict mode or loose mode based on allowlist config |
| loose | allows all environment variables |
| strict | only allow declared variables |
| option | description |
| ----------------------------- | -------------------------------------------------------------------------------------------------------- |
| infer | infers loose or strict-include-framework-vars mode based upon existence of configuration |
| loose | allows all environment variables |
| strict-include-framework-vars | allows only environment variables detected from your framework and those specified by your configuration |
| strict | allows only environment variables specified by your configuration |

`PATH`, `SHELL`, and `SYSTEMROOT` are always available to all tasks.

##### `infer`

In infer mode, Turborepo will look for [`experimentalPassThroughEnv`][1] for
each task config, and [`experimentalGlobalPassThroughEnv`][2] in the root of the
`turbo.json` config. If either is defined, "strict" mode is inferred. If
neither is defined, then `loose` mode is inferred.
`turbo.json` config. If either is defined, "strict-include-framework-vars" mode
is inferred. If neither is defined, then `loose` mode is inferred.

In this mode, the value of `experimentalGlobalPassThroughEnv` and the flag's
value ("infer") will only be incorporated into the global hash if
Expand All @@ -210,9 +211,25 @@ value ("infer") will only be incorporated into the global hash if
##### `loose`

In loose mode, all environment variables are made available to the task.
The value of `experimentalGlobalPassThroughEnv` and the flag's value ("loose")
The flag's value ("loose") itself will be incorporated into the global hash.

##### `strict-include-framework-vars`

In strict-include-framework-vars mode, environment variables specified in the following keys are
available to the task:

- `env` and `experimentalPassThroughEnv` in each task configuration
- `globalEnv` and `experimentalGlobalPassThroughEnv` in the root of your config

In addition, values detected by [automatic environment variable inclusion](../caching#automatic-environment-variable-inclusion)
will also be available to the task.

The value of `experimentalGlobalPassThroughEnv` and the flag's value ("strict-include-framework-vars")
itself will be incorporated into the global hash.

If strict-include-framework-vars mode is specified or inferred _all_ tasks are run in
strict-include-framework-vars mode regardless of their configuration.

##### `strict`

In strict mode, only environment variables specified in the following keys are
Expand All @@ -224,7 +241,7 @@ available to the task:
The value of `experimentalGlobalPassThroughEnv` and the flag's value ("strict")
itself will be incorporated into the global hash.

If strict mode is specified or inferred, _all_ tasks are run in strict mode,
If strict mode is specified or inferred _all_ tasks are run in strict mode,
regardless of their configuration.

#### `--filter`
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"name": "next-app",
"scripts": {
"build": "echo \"hello from $NEXT_PUBLIC_HELLO\""
},
"dependencies": {
"next": "latest"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"name": "vite-app",
"scripts": {
"build": "echo \"hello from $VITE_HELLO\""
},
"dependencies": {
"vite": "latest"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"name": "vue-app",
"scripts": {
"build": "echo \"hello from $VUE_APP_HELLO\""
},
"dependencies": {
"@vue/cli-service": "latest"
}
}