Add environment variable support for config files#547
Add environment variable support for config files#547kapral18 wants to merge 3 commits intosorenlouv:mainfrom
Conversation
cf8528d to
2fc6e14
Compare
docs/config-file-options.md
Outdated
|
|
||
| ```json | ||
| { | ||
| "accessToken": "${GITHUB_ACCESS_TOKEN}" |
There was a problem hiding this comment.
I'm not sure this is the right approach. I think selected environment variables should be supported - like GITHUB_ACCESS_TOKEN but it shouldn't require updating the config.
If GITHUB_ACCESS_TOKEN is set it should be used as a fallback if there's no access token in the config.
There was a problem hiding this comment.
Btw the config management is currently extremely messy. There are config options, cli options, "module" options, and now also environment options.
I have been considering updating the config management. Moving all config definitions (and defaults) to a single file and using zod for validation instead of relying on yargs for everything. yargs should just be used to parse the cli args.
There was a problem hiding this comment.
can you check if this new commit is closer to what you want? Updated PR as well
7a894a3 to
8037e59
Compare
src/options/cli-args.test.ts
Outdated
| }); | ||
| }); | ||
|
|
||
| describe('zod validation', () => { |
There was a problem hiding this comment.
zod should be an implementation detail. See how the other args are tested
There was a problem hiding this comment.
✅ Done - removed all zod tests from this file
src/options/config-schema.ts
Outdated
| // Schema for partial config (from files/cli) - without defaults applied | ||
| // This is used for parsing CLI args and config files where we don't want defaults | ||
| // Defaults are applied later via manual merge in options.ts | ||
| export const partialConfigSchema = baseConfigSchema.loose(); |
There was a problem hiding this comment.
Why is this change needed for adding environment variable support?
There was a problem hiding this comment.
I was attempting to add zod-based validation for your earlier comment about config management being messy. My schema validated all serializable options but excluded non-serializable fields like autoFixConflicts (function handlers can't be in config files). You're right that this isn't truly comprehensive validation - it's partial validation of the serializable subset. Since that's outside the scope of the env var feature, I've removed all the zod code.
| ); | ||
| }); | ||
|
|
||
| it('should include BACKPORT_ACCESS_TOKEN documentation in template', async () => { |
There was a problem hiding this comment.
This test seems really unnecessary. It test the existence of a comment?
There was a problem hiding this comment.
My thinking was if it's part of the public interface it can be tested, but either way I'm okay with removing it. Removed.
| import { BackportError } from '../../lib/backport-error'; | ||
| import { parseConfigFile, readConfigFile } from './read-config-file'; | ||
|
|
||
| describe('parseConfigFile', () => { |
| }); | ||
| }); | ||
|
|
||
| describe('zod validation', () => { |
There was a problem hiding this comment.
As mentioned above, zod should be an implementation detail
There was a problem hiding this comment.
✅ Done - removed all zod validation tests
| const originalEnv = process.env; | ||
|
|
||
| beforeEach(() => { | ||
| process.env = { ...originalEnv }; | ||
| mockGithubConfigOptions({ | ||
| viewerLogin: 'testuser', | ||
| }); | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| process.env = originalEnv; | ||
| }); |
There was a problem hiding this comment.
Why are you calling mockGithubConfigOptions?
| const originalEnv = process.env; | |
| beforeEach(() => { | |
| process.env = { ...originalEnv }; | |
| mockGithubConfigOptions({ | |
| viewerLogin: 'testuser', | |
| }); | |
| }); | |
| afterEach(() => { | |
| process.env = originalEnv; | |
| }); | |
| afterEach(() => { | |
| delete process.env.BACKPORT_ACCESS_TOKEN; | |
| }); |
There was a problem hiding this comment.
I was setting up the mock out of habit from other tests, but it's not needed here since we're only testing the env var fallback. Fixed - removed the unnecessary mock call.
src/options/options.ts
Outdated
| const resolvedAccessToken = | ||
| // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing | ||
| accessToken?.trim() || | ||
| // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing | ||
| process.env.BACKPORT_ACCESS_TOKEN?.trim() || | ||
| undefined; |
There was a problem hiding this comment.
Rename to trimmedAccessToken. "resolved" makes it sounds like an async value
| const resolvedAccessToken = | |
| // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing | |
| accessToken?.trim() || | |
| // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing | |
| process.env.BACKPORT_ACCESS_TOKEN?.trim() || | |
| undefined; | |
| const trimmedAccessToken = | |
| // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing | |
| accessToken?.trim() || process.env.BACKPORT_ACCESS_TOKEN?.trim(); |
src/options/config-schema.ts
Outdated
| help: z.boolean().optional(), | ||
| version: z.boolean().optional(), | ||
| v: z.boolean().optional(), | ||
| }); |
There was a problem hiding this comment.
I'd like to introduce zod, but in that case it should replace the the existing validation. I think that's outside the scope of this change
There was a problem hiding this comment.
Agreed - removed all zod code. The PR now focuses solely on the BACKPORT_ACCESS_TOKEN fallback feature.
Adds support for environment variable substitution in both global
(.backport/config.json) and project (.backportrc.json) configuration
files using the syntax ${VARIABLE_NAME}.
Features:
- Substitutes ${VAR} with process.env.VAR value
- Fails fast with clear error if variable is undefined
- Fails fast with clear error if variable is empty string
- Consistent with codebase's fail-fast validation philosophy
- Particularly useful for CI/CD environments (GitHub Actions)
Benefits:
- Keep secrets out of version control
- Share config files across teams safely
- Works seamlessly in CI/CD pipelines
- Clear error messages for missing/empty variables
Example usage:
{
"accessToken": "${GITHUB_ACCESS_TOKEN}",
"repoOwner": "elastic",
"repoName": "kibana"
}
Includes comprehensive test coverage (18 test cases) and
documentation updates.
…lback
This commit completely replaces the original ${VARIABLE_NAME} substitution
approach with a more focused solution: automatic fallback to the
BACKPORT_ACCESS_TOKEN environment variable when accessToken is missing,
empty, or whitespace-only.
Changes:
- Remove ${VAR} substitution logic from config file parsing
- Add BACKPORT_ACCESS_TOKEN environment variable fallback in options.ts
- Add zod for config validation (validates CLI args and config files)
- Centralize config schema definitions in config-schema.ts
- Use partialConfigSchema for entry point validation
- Use getDefaultConfigOptions() for explicit default merging
- Update global config template to document BACKPORT_ACCESS_TOKEN
- Add comprehensive test coverage for fallback behavior
Design decisions:
- Validate early at entry points (CLI args, config files)
- Apply defaults via explicit manual merge (not zod)
- Maintain clear precedence: CLI > remote > config > env > defaults
- Use || operator for fallback (treats empty/whitespace as missing)
- Allow accessToken key to be omitted entirely from config files
The zod implementation uses a clean architecture:
- baseConfigSchema (internal): All fields optional, no defaults
- partialConfigSchema (exported): Used for validation at entry points
- getDefaultConfigOptions(): Single source of truth for defaults
- Manual merge in options.ts: Explicit, debuggable precedence
Schema validation scope:
- Validates all serializable config options (CLI args, config files)
- Intentionally excludes non-serializable fields (e.g., autoFixConflicts function handler)
- Includes deprecated options for backward compatibility
- No breaking changes: all fields remain optional as before
Test coverage:
- 130 tests pass (6 test suites in src/options/)
- BACKPORT_ACCESS_TOKEN fallback: 7 test cases
- Zod validation in cli-args: 6 test cases
- Zod validation in read-config-file: 10 test cases
- Global config documentation: 1 test case
8037e59 to
4d48f0d
Compare
Summary
Adds automatic fallback to
BACKPORT_ACCESS_TOKENenvironment variable whenaccessTokenis missing/empty in config files. Also introduces Zod for config validation.Key Changes
Environment Variable Fallback:
BACKPORT_ACCESS_TOKENwhenaccessTokenis missing, empty, or whitespaceaccessTokenkey can be omitted entirely from config filesZod Validation:
partialConfigSchema: validates structure without applying defaultsgetDefaultConfigOptions(): single source of truth for defaultsoptions.ts: explicit, debuggable precedenceSchema Scope (No Breaking Changes):
autoFixConflictsfunction handler)Why This Approach?
BACKPORT_ACCESS_TOKEN vs ${VAR} substitution:
Validate at entry, merge manually:
Exclude non-serializable fields:
autoFixConflicts) only available via module APIUsage
Config file:
{ // Omit accessToken or leave empty "accessToken": "" }Environment:
CLI override:
backport --accessToken="ghp_different"Testing
130 tests pass (6 test suites):
Migration
None needed - existing configs work as before. To use env variable: set
BACKPORT_ACCESS_TOKENand omit/emptyaccessTokenin config.Commits
${VAR}substitution (kept for reference)BACKPORT_ACCESS_TOKEN+ Zod