Skip to content

Add --filter flag and @tag() decorator for selecting env vars#873

Merged
theoephraim merged 15 commits into
mainfrom
add-filter-flag-and-tag-decorator
Jul 14, 2026
Merged

Add --filter flag and @tag() decorator for selecting env vars#873
theoephraim merged 15 commits into
mainfrom
add-filter-flag-and-tag-decorator

Conversation

@theoephraim

@theoephraim theoephraim commented Jul 8, 2026

Copy link
Copy Markdown
Member

Summary

  • Add --filter to varlock load/run for selecting/omitting env vars by key name, glob (STRIPE_*), negation (!KEY), decorator (@sensitive/@required), or tag (#tagname)
  • Add a new @tag() item decorator to attach tags to items, used by #tagname selectors. Tag names are validated (start with a letter/number, then letters/numbers/_/-) so every tag stays selectable via #tagname; invalid names and empty @tag() calls are schema errors, and duplicates collapse
  • Add a matching filter= arg to @generate* code-generation decorators (@generateTsTypes, @generatePythonEnv, etc.), so one schema can emit multiple generated files scoped to different subsets (e.g. a billing-only env.d.ts)
  • On run, a filtered-out var can't reach the child process at all: excluded items are stripped from the __VARLOCK_ENV blob (including its override-provenance metadata) and removed from the child env even when set ambiently (same treatment as @internal). Without the ambient stripping, SECRET=x varlock run --filter='!@sensitive' -- ... passed the ambient value through process.env and, since excluded items are left out of the redaction map, printed it unredacted
  • Add a _VARLOCK_FILTER env var fallback for --filter, for wrapper scripts, CI config, or build-time integrations that don't go through the CLI (an explicit --filter flag still takes precedence)
  • A key/glob/tag-only --filter now scopes resolution and validation too, not just output: load/run resolve only the selected items plus their transitive dependencies, so an unrelated broken item outside the filter no longer blocks the load - e.g. a build step scoped to --filter="#frontend" doesn't care if a backend-only var is misconfigured. @sensitive/@required selectors can't be scoped this way (matches aren't knowable until the graph is resolved), so those still resolve and validate everything, same as before.
  • Docs spell out how selectors of different kinds combine: non-negated selectors OR together regardless of kind, then negated selectors subtract from that whole set (no intersection is expressible)
  • A --filter that matches no items (e.g. a typo'd tag) warns on stderr; the command still succeeds and stdout stays clean for piping
  • VSCode extension: autocomplete/hover entries for @tag(), the filter= arg on @generate* decorators, and @internal (pre-existing gap)

Credit to @Jarva for #872, which proposed --sensitive/--non-sensitive flags on load for piping secrets to tools like fly secrets import. This PR generalizes that idea into a single --filter flag that also covers @required, tags, and key/glob selection.

Fixed along the way: the codegen filter= arg initially evaluated @sensitive/@required against raw ConfigItem getters, which aren't populated until resolveEnvValues() runs - but code generation runs before that, so those selectors silently matched everything. Filtering now goes through the same pre-resolution computation (getTypeGenInfo()) codegen already uses for isSensitive/isRequired.

Rebased on top of #874 (merged), which fixes load --format json-full to exclude @internal items by default - discovered while documenting --filter's interaction with @internal. load.command.ts's json-full branch now combines both fixes (includeInternal + filterKeys); verified end-to-end that --include-internal and --filter compose correctly together.

Test plan

  • bun run --filter varlock typecheck
  • bunx vitest run in packages/varlock (1000 tests passing, including coverage for the filter parser, @tag() decorator, codegen filter= wiring, mixed-kind selector interaction, @internal exclusion holding regardless of filter, the _VARLOCK_FILTER env var/precedence, scoped resolution/validation for key/glob/tag filters, and getSerializedGraph filterKeys/override-provenance exclusion), plus smoke tests covering run --filter ambient-env stripping
  • bun run lint:fix
  • bun run --filter varlock-website astro build
  • Manually verified varlock load --filter=..., varlock run --filter=..., varlock codegen with mixed filter="#tag,@sensitive", _VARLOCK_FILTER (plus flag precedence), scoped resolution (an unrelated invalid item outside a key/tag filter no longer fails the load, while --filter=@sensitive still catches it), and --include-internal + --filter composed together end-to-end against real .env.schema files

@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

bumpy-frog

The changes in this PR will be included in the next version bump.

minor Minor releases

  • varlock 1.10.0 → 1.11.0

patch Patch releases

  • env-spec-language 0.2.5 → 0.2.6

Bump files in this PR

Click here if you want to add another bump file to this PR


This comment is maintained by bumpy.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 8, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
varlock-website c4cf401 Commit Preview URL

Branch Preview URL
Jul 14 2026, 10:57 PM

@pkg-pr-new

pkg-pr-new Bot commented Jul 8, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/varlock@873

commit: c4cf401

@philmillman philmillman left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

lgtm, let's consider an in-depth tags and filters guide for the future

…nv vars

Adds --filter to `varlock load`/`run` for selecting/omitting items by key
name, glob, negation (!KEY), decorator (@sensitive/@required), or tag
(#tagname). Tags are set via a new @tag() item decorator.

The same selector language is also available as a filter= arg on
@generate* code-generation decorators, so one schema can emit multiple
generated files scoped to different subsets (e.g. a billing-only env.d.ts).
Spell out that non-negated selectors OR together regardless of kind
(key/glob, @decorator, #tag), while negations subtract from that whole
set rather than being scoped to a specific positive selector.
…ter bug

Added tests for how key/glob, @decorator, and #tag selectors combine
(union of positives, then subtract negatives, regardless of kind).

The codegen-side mixed-kind tests caught a real bug: the `filter=`
codegen arg was matching @sensitive/@required against bare ConfigItem
getters, which aren't populated until resolveEnvValues() runs - but
code generation runs before that. Fixed by filtering against
TypeGenItemInfo (computed pre-resolution by getTypeGenInfo(), the same
source codegen already uses for isSensitive/isRequired) instead, and
widened the shared filter engine to a structural FilterableItem type
so it can operate on either shape.
Regression coverage confirming @internal items stay excluded from
getResolvedEnvObject/getSerializedGraph and codegen's collectTypeGenItems
even when a filter would otherwise match them (e.g. by exact key name) -
only --include-internal can opt an item back in.
Lets --filter be set without a CLI flag - useful for wrapper scripts,
CI config, or build-time integrations that call varlock's loading APIs
directly instead of going through the CLI. An explicit --filter flag
always takes precedence, matching the existing _VARLOCK_REDACT_STDOUT
precedent.
…longside this PR

Removes the "known gap" caution about framework integrations not
enforcing @internal exclusion - #874 fixes that at the CLI level
(load --format json-full excludes internal items by default), so
every integration inherits the guarantee with no code changes of
their own. Both PRs are being merged together.
…ag selectors

load/run previously always resolved (and validated) the entire graph
regardless of --filter, so an unrelated invalid item outside the
filter would still block a load - even though it wasn't going to be
output anyway. A --filter using only key names/globs/tags is fully
knowable from the schema before resolution, so we now resolve just
those items plus their transitive dependencies, letting an unrelated
broken var stay broken without blocking (e.g. scoping a build's
validation to `--filter="#frontend"` independent of the backend vars).

@sensitive/@required selectors can't be scoped this way - matches
aren't knowable until the graph is already resolved - so those still
fall back to resolving (and validating) everything, same as before.
@theoephraim theoephraim force-pushed the add-filter-flag-and-tag-decorator branch from 0f270a6 to be57739 Compare July 11, 2026 22:45
…hen set ambiently

An ambiently-set value for a filtered-out schema key passed straight through
the process.env spread to the child (and, being excluded from the redaction
map, printed unredacted) - contradicting the intent that a filtered-out var
can't reach the child process. Strip excluded schema keys from the child env,
mirroring the @internal handling, and drop them from the blob's override
provenance metadata too.

Also docs fixes: json-full does not show @internal items by default (needs
--include-internal), em dash cleanup, codegen filter= cross-reference.
Replace the resolveItemFilterKeys/planFilterResolution pair (which each
re-parsed the filter string, re-read _VARLOCK_FILTER, and duplicated the
SchemaError->CliExitError wrapping) with a single parse-once getCliItemFilter()
returning getResolveKeys/getFilterKeys. Bad filter syntax now errors before any
loading work. Also precompile glob regexes per token instead of per item, and
collapse codegen's unfiltered/filtered field caches into one map keyed by
filter string. No behavior changes.
…lter= arg

The hardcoded decorator catalog didn't know about the new @tag() decorator or
the filter= arg on @generate* decorators (added in #873), and @internal was
missing from the item-decorator list (pre-existing gap). Purely a
completion/hover gap - there are no unknown-decorator diagnostics.
A typo'd key or tag silently produced empty output on load, or a child
process with no schema vars on run, with exit 0. Warn on stderr (stdout
stays clean for piped json/env/shell output); the command still succeeds.
Tag names must start with a letter or number, then letters, numbers, _, -,
or . - rejecting the characters the filter language reserves (comma
separator, !/@/# selector prefixes, whitespace, globs). Invalid names and
empty @tag() calls are schema errors; #tag selectors in --filter/filter=
enforce the same rule with a clear error instead of silently matching
nothing. Duplicate tags now collapse.
Tag names are now letters/numbers/_/- only (still must start with a letter
or number). Loosening later is backward-compatible; tightening would not be.
@theoephraim theoephraim merged commit 5607f3b into main Jul 14, 2026
27 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants