Repository navigation
RFC: Pluggable task environment providers #37105
yrybak-op8
started this conversation in
Feature Requests
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Summary
Nx's task environment is currently sourced from exactly two places:
process.envand.env*files(
getTaskSpecificEnv/loadDotEnvFilesForTaskinpackages/nx/src/tasks-runner/task-env.js). There is nosupported extension point for other environment sources — secret managers (Doppler, Vault, 1Password, AWS
Secrets Manager, GCP Secret Manager), remote config services, or any tool that needs to inject task-scoped
variables computed at run time rather than read from a static file.
This RFC proposes a first-class, pluggable task environment provider API so plugin authors can inject
variables into a task's environment the same way Nx already lets them contribute targets, executors, and
project graph nodes — without patching Nx itself.
Problem
The only executor with a documented way to attach environment variables to a specific target is
nx:run-commands, via itsenvFile/envoptions —and that's a property of that one executor's own schema, not something Nx provides generically. Framework
executors (
@nx/vite:build,@nx/storybook:build,@nx/webpack:build/ssr-dev-server, etc.) have noequivalent option in their schemas. For those, the only two ways to get a variable into the task are:
nx(shell export, CI job env, etc.), or.env*file, which Nx loads workspace/project-wide whenNX_LOAD_DOT_ENV_FILESis set.Both are all-or-nothing for the whole
nxinvocation: there's no per-target scoping, and neither supports avalue that has to be resolved at run time from something other than a file (a secrets manager, a remote config
service, computed build metadata, etc.) without first writing it to disk or exporting it into the parent shell
before Nx even starts.
This isn't specific to JS/TS executors, either — non-JS language plugins (
@nx/dotnet,@nx/gradle,@nx/maven) go through the same coreprocess.env+.env*mechanism. A hook in that one shared pipelinewould benefit every one of these ecosystems at once, not just JS/TS.
Motivation
We tried integrating a secrets manager as a task-scoped environment source and ran into requirements that
.envfiles andprocess.envalone can't express:.envand inherited shell values,the values themselves ever reaching a remote cache,
None of this fits
.envfiles: the values aren't static, aren't safe to persist to disk, and the requirementsabove need to be expressed somewhere richer than dotenv syntax.
Current state and why it isn't a supported extension point
Looking at
packages/nx/src/tasks-runner/task-env.js,getTaskSpecificEnvdoes exactly this, in order:process.env(unloadDotEnvFiles),.env*files for the task (NX_LOAD_DOT_ENV_FILES === 'true'),getEnvVariablesForTasklater merges under Nx's own internal env vars.There is no hook between steps 2 and 3, no plugin lifecycle event fires with the resolved task and project
graph before the child process spawns, and no
TaskEnvironmentProvidertype is exported.To get this working at all, we ended up carrying a local patch on top of Nx that:
(task, graph, env) => env | voidinsidegetTaskSpecificEnv, afterdotenv loading, before Nx's own overrides,
Promise(synchronous only, since this sits on the hot path ofhash computation, not just process spawn).
This works, but every Nx upgrade touching
tasks-runner/task-env.jsis a patch to re-review, and anyone elsewho wants the same capability has to discover and re-derive an equivalent patch independently. A patched
dependency is also invisible to anyone auditing the repo's actual dependency tree.
Why the existing
preTasksExecutionhook isn't enoughNx plugins already get an async
preTasksExecution(options, context)lifecycle hook, and we do use it for thepart of this that fits: setting a handful of variables once, globally, before the run starts, by mutating
process.envdirectly. It falls short of per-task injection for a few reasons:nxinvocation, not once per task, and has no access to the individualTaskor wherein the project graph it sits — so it cannot express "these variables for target A, those for target B" within
the same
nx run-many,process.envinside it is global for the whole process, which any task can read — it can'tscope a value to only the tasks that declared they need it,
per-task hash the way
inputs: [{ "env": "..." }]does — anything it sets ends up as ambient environment, notas a tracked, cache-affecting input tied to a specific task,
getTaskSpecificEnv(where per-task env isactually assembled) is synchronous and runs on the hot path of hash computation, so a hook meant to plug into
it needs the same synchronous contract our patch enforces.
In short,
preTasksExecutioncovers "inject some global config once at the start," which is a real and usefulcase, but not "resolve different values per task, tied into that task's own hash." That second case is what
this RFC is about, and it needs a hook that sees the
Taskitself, not just the workspace root.Proposed API
Expose this as a documented Nx plugin lifecycle hook, analogous to
preTasksExecution/postTasksExecutionin
nx/src/project-graph/plugins/public-api, rather than an ad hoc env var pointing at a module path.Deliberately, the hook does not receive the environment assembled so far (
.envvalues, other plugins'contributions). A provider only returns the variables it wants to add — Nx merges those on top afterwards. If
the hook received the running environment, one plugin's provider could read values injected by
.envor byanother provider for an unrelated purpose (e.g. a secrets-manager plugin reading a variable some other plugin
resolved from its own vault), which defeats the point of scoping a value to only the tasks that declared they
need it. Keeping providers write-only relative to each other is what makes composing multiple
provideTaskEnvironmentplugins on the same task safe.Plugins would register it the same way they already register
createNodes/createDependenciesinnx.json#plugins.getTaskSpecificEnvwould fold every registeredprovideTaskEnvironmenthook's return valueinto the pipeline after
.envloading (multiple plugins could compose, each contributing disjoint variables).The hook would stay synchronous, for the same hot-path reason our patch enforces it today.
One smaller, related gap fell out of trying to build this against real infrastructure: cache-affecting env
inputs only version known variable names (
inputs: [{ "env": "SOME_VAR" }]). A provider that injects adynamic, allow-list-defined set of variables needs a way to say "hash whatever this plugin injected" without
enumerating every name in
project.jsonup front.What this would unlock
With
provideTaskEnvironmentin place, a community or first-party@nx/dopplerplugin (or the equivalent forVault, 1Password, etc.) could exist as a normal, installable Nx plugin rather than something every workspace
re-implements against a patched dependency:
doppler.yaml(Doppler's own convention for mapping a directory to aproject/config) to know which Doppler project backs which part of the workspace,
configurationswould select which Doppler config to pull from (e.g. aproductionconfiguration ona target maps to that project's production config) — reusing a concept Nx already has, instead of introducing
parallel environment-selection syntax,
doppler:run-commandexecutor would exist as a drop-in alternative tonx:run-commandsfor targets thatare genuinely "run one shell command," injecting Doppler secrets the same way
nx:run-commands' ownenvFileinjects a dotenv file's contents,provideTaskEnvironmentwould be what makes that same injection reach targets that use a framework executorinstead (
@nx/vite:build,@nx/storybook:build, etc.) — those have noenv/envFileoption of their ownand can't switch to
doppler:run-command, since that would mean giving up the executor itself,.envfile on disk or exporting them into the OS-level shellenvironment before Nx starts — values would be resolved and injected per task, scoped to exactly the process
that needs them.
That's the shape of the plugin this RFC is trying to make possible; it isn't proposing Nx ship Doppler support
itself, only the hook that lets a plugin like this be built without patching Nx.
Alternatives considered
nxinvocation (e.g.secrets-cli run -- nx run app:build): works for asingle top-level invocation, but can't scope differently per target within one
nx run-many, and doesn'tparticipate in cache-hashing since the wrapping happens entirely outside Nx's task graph.
real executor): loses the target's original executor/schema/options/cache metadata, and has to be re-applied
to every target that needs secrets instead of being declared once.
nx migrate, requires re-patching on every relevant Nx upgrade, and isn't discoverable by anyone building asimilar integration.
All reactions