From d44feeb2a75faa95c3594ddddbfe9e2e27b6a473 Mon Sep 17 00:00:00 2001 From: Theo Ephraim Date: Tue, 16 Jun 2026 13:47:49 -0700 Subject: [PATCH] fix(varlock): preserve typed builtin vars referenced from root decorators Builtin vars like VARLOCK_IS_CI declare a type (boolean), but when registered early via a root-decorator reference (e.g. @import/@initOp args), the finishLoad process() pass recomputed the type and defaulted it back to 'string', stringifying false -> "false". That made not()/if() treat it as a truthy string. The builtin resolver now advertises its declared type via inferredType so config-item type inference preserves it. --- .bumpy/fix-builtin-var-type-preservation.md | 5 +++ .../varlock/src/env-graph/lib/env-graph.ts | 6 +++ .../src/env-graph/test/builtin-vars.test.ts | 38 +++++++++++++++++++ 3 files changed, 49 insertions(+) create mode 100644 .bumpy/fix-builtin-var-type-preservation.md diff --git a/.bumpy/fix-builtin-var-type-preservation.md b/.bumpy/fix-builtin-var-type-preservation.md new file mode 100644 index 00000000..25fd92e8 --- /dev/null +++ b/.bumpy/fix-builtin-var-type-preservation.md @@ -0,0 +1,5 @@ +--- +varlock: patch +--- + +Fix typed builtin vars (e.g. boolean VARLOCK_IS_CI) being stringified when referenced from root decorators like @import/@initOp, which broke not()/if() logic diff --git a/packages/varlock/src/env-graph/lib/env-graph.ts b/packages/varlock/src/env-graph/lib/env-graph.ts index 49cf94af..48db206c 100644 --- a/packages/varlock/src/env-graph/lib/env-graph.ts +++ b/packages/varlock/src/env-graph/lib/env-graph.ts @@ -293,6 +293,12 @@ export class EnvGraph { const BuiltinVarResolver = createResolver({ name: `\0builtin:${key}`, description: builtinDef.description, + // Advertise the builtin's declared type so that if the item gets a + // process() call (e.g. when registered early via a root-decorator + // reference), config-item type inference preserves it instead of + // defaulting back to 'string' — which would stringify a boolean/number + // builtin (e.g. VARLOCK_IS_CI false -> "false", breaking not()/if()). + inferredType: builtinType, async resolve() { return builtinDef.resolver(graph.ciEnvInfo, graph.processEnvForBuiltins); }, diff --git a/packages/varlock/src/env-graph/test/builtin-vars.test.ts b/packages/varlock/src/env-graph/test/builtin-vars.test.ts index f63b4704..c2f94121 100644 --- a/packages/varlock/src/env-graph/test/builtin-vars.test.ts +++ b/packages/varlock/src/env-graph/test/builtin-vars.test.ts @@ -1,6 +1,7 @@ import { describe, test, vi, expect, } from 'vitest'; +import outdent from 'outdent'; import { envFilesTest } from './helpers/generic-test'; // We need to mock the child_process module used by builtin-vars.ts @@ -43,6 +44,43 @@ describe('VARLOCK_* builtin variables', () => { })); }); + describe('type preservation', () => { + // Regression: when a typed builtin (e.g. boolean VARLOCK_IS_CI) is registered + // early via a root-decorator reference, the finishLoad process() pass used to + // recompute its type and default it back to 'string', stringifying `false` to + // "false" — which made not()/if() see a truthy string. The builtin resolver now + // advertises its declared type via inferredType so the type is preserved. + test('boolean builtin referenced in a root decorator keeps its boolean type', envFilesTest({ + files: { + '.env.schema': outdent` + # @import(./.env.imported, enabled=not($VARLOCK_IS_CI)) + # --- + LOCAL_VAR=local + `, + '.env.imported': 'IMPORTED_VAR=imported', + }, + processEnv: {}, + expectValues: { + VARLOCK_IS_CI: false, // boolean false, not the string "false" + IMPORTED_VAR: 'imported', // import is enabled because not(false) === true + }, + })); + + test('same root-decorator reference disables the import when in CI', envFilesTest({ + files: { + '.env.schema': outdent` + # @import(./.env.imported, enabled=not($VARLOCK_IS_CI)) + # --- + LOCAL_VAR=local + `, + '.env.imported': 'IMPORTED_VAR=imported', + }, + processEnv: { CI: 'true', GITHUB_ACTIONS: 'true' }, + expectValues: { VARLOCK_IS_CI: true }, + expectNotInSchema: ['IMPORTED_VAR'], + })); + }); + describe('VARLOCK_ENV environment detection', () => { test('detects test environment from NODE_ENV=test', envFilesTest({ envFile: 'MY_ENV=$VARLOCK_ENV',