Repository navigation
Releases: taljacob2/config-transform
Release list
0.28.0-alpha
Released before the Key Vault support has been checked against a real vault, at the repo owner's
request, so a design partner can install it. docs/KEYVAULT_VERIFICATION.md is that check; anything
it finds goes into a follow-up release.
Added
- Azure Key Vault as a secrets source (
docs/KEYVAULT_SECRETS_DESIGN.md), so access to secret
values can be granted per vault (one per repo × environment × client) through Azure RBAC instead
of one git-crypt key for every client. A layer'ssecretscan list, next to*.secret.envfiles:
"keyvault://<vault>"(everyCFSECRET-…secret in it),"keyvault://<vault>/CFSECRET-…"(one
secret, one value),"keyvault://<vault>/<other-name>"(one secret holding.envtext), or
{ "from": "keyvault://<vault>/<secret>", "as": "CFSECRET_…" }(one secret under an explicit
placeholder name). A resource'sreplacecan be"keyvault://<vault>/<secret>"— the whole
file, such as a Firebase service account; a certificate's secret is written as its real bytes.
Same layer precedence, environment-variable override, report tree and all-or-nothing real runs
as files; the tree names each value's exact secret
(patched in: keyvault://kv-ra-prod-ca/CFSECRET-SMTP-PASSWORD).- Signs in through
az(az login;azure/loginwith OIDC in GitHub Actions;
az login --identityin Azure), orAZURE_*service-principal/workload-identity environment
variables — never interactively, and only to the signed-in tenant. - Previews and
--listread names and metadata only (except.envtext, whose names are inside
its value); values are read only for a real run or--reveal-secrets. A run whose resources use
no placeholder never contacts Azure. - A vault that can't be read is
unknown, with the reason: not signed in,403 ForbiddenByRbac,
blocked by the vault's firewall, another tenant, can't be reached. A disabled or expired secret
shows why it doesn't count. A named secret that doesn't exist is an error. - Azure's own error messages are never printed (they can carry response bodies), and errors from
.envtext never quote a line or a key. - A new project,
ConfigTransform.Secrets.AzureKeyVault, holds the only Azure dependencies
(Azure.Security.KeyVault.Secrets,Azure.Identity).
- Signs in through
Changed
- Secret names must be upper snake case (
docs/SECRETS_DESIGN.mddecision #28). A placeholder,
*.secret.envkey orasname with a lower-case letter — including{{cfsecret_…}}, which used
to stay in the output as plain text — is an error naming the fix (Try: {{CFSECRET_DB}}). The
sources disagreed about case: an environment variable matches exactly on Linux but not on
Windows, and Key Vault ignores case. - A real run's unresolved-secrets error now suggests
az loginand vault access alongside
git-crypt unlock.
0.27.0-alpha
Changed
- Each secret gets its own tree in the resolution report, after the resource's chain and read
the same way: its state (resolved/MISSING/unknown),used in:(each file of the chain
that writes its placeholder), then every layer and finally the environment variable, each
patched in: <file>ornot patched in. The old one line per secret showed only the winning
file, so an override was invisible. A locked git-crypt file shows at its own layer
(unknown: <file> is locked), and the environment-variable step is shown even when unset, so a
misspelled CI variable is visible — one set but empty says it counts as unset. Names, files and
variable names only, never a value. From a real user's review of the pilot's output; see
docs/SECRETS_DESIGN.md's "What each mode does" and decisions #24–#27. --listends with the same tree, once per secret any of the layer's resources uses, found by
scanning their base files and patches. Replaced resources and resources no engine handles are
left out (neither is ever substituted).- The chain's first step is labelled
resource, notbase, in both the report and--list.
Fixed
--list's header listed every secrets file in the chain under the target layer, as if the
target declared them. It now lists only the target layer's ownsecrets; the rest appear in
the secrets tree, at the layer that lists them.--list --reveal-secretswas silently accepted and did nothing; it's now an error with a
Try:hint, since--listnever shows a value.
0.26.0-alpha
Added
- YAML
setmatches or creates an item in an array of objects — the lastsetgap. The same
command and$elemMatchoverlay shape as JSON (--match key=Rules --match role=Admin --set enabled=truewrites a patch, never an array position), resolved at merge time against the
document as merged so far, with the same upsert, ambiguity and same-item rules
(YamlElemMatchResolver, ported fromJsonElemMatchResolver). One deliberate difference:
conditions compare by text, not type — an unquoted YAML scalar's type depends on the reader, so
enabled=truematches bothenabled: trueandenabled: "true". Valuessetwrites follow
YAML's value-typing rule. Seedocs/FIELD_AUTHORING_DESIGN.md's "YAML array-of-objects matching".
0.25.0-alpha
Added
init --template secrets: an opt-in starter-tree variant that demonstrates secrets
(docs/SECRETS_DESIGN.md) and runs on the first command — a{{CFSECRET_DEMO_API_KEY}}
placeholder in each Environment patch, environment-leveldemo.secret.envfiles with a
Client-A/Production override, and one whole-file secret viareplace
(configtransform-template-credentials.json). Every value is obviously fake. Because the
*.secret.*files are plaintext until git-crypt covers them,initprints how to set that up —
and never writes.gitattributesitself, since a git-crypt rule on a machine without git-crypt
breaksgit add. Never part of the default template. Thedefaultandhostsvariants are
unchanged byte for byte. Seedocs/INIT_COMMAND_DESIGN.md's "Thesecretsvariant".
0.24.1-alpha
Fixed
setno longer reformats numbers it was given as text, and YAMLsetalways quotes strings.
Prompted by a question about YAML's "Norway problem" (YAML 1.1 reading an unquotedNOas
boolean false); checking every write path found three real issues.set(JSON and YAML) treated
anything that parses as a number as one, so--set 02134wrote2134,007wrote7,
1.10wrote1.1and1e3wrote1000; now a value is a number or boolean only when it reads
back exactly as typed (5432,1.5,true), and anything else stays a string. YAMLset
wrote strings unquoted, so--set NO/yes/onbecame booleans to YAML 1.1 readers and
--set null/~a real null; strings are now always double-quoted. And YAMLsetre-serialized
the whole target file, losing its quoting style; it now edits only the value it sets, leaving
every other line exactly as it was. Secret substitution had the same gap: a plain YAML scalar it
changed could come out as an unquotedNO; such scalars are now written double-quoted. The merge
itself is unchanged — it carries over exactly what an author wrote. See
docs/FIELD_AUTHORING_DESIGN.md's "Value typing".
0.24.0-alpha
Added
- Secrets, stage 1:
{{CFSECRET_NAME}}placeholders (docs/SECRETS_DESIGN.md). Config files and
patches can hold placeholders whose values live in encrypted*.secret.envfiles a layer lists
under a newsecretsfield, so git-crypt only needs to cover.configtransform/**/*.secret.*and
every other overlay stays readable and reviewable. Values follow the layer chain (later layers
override), an environment variable with the secret's exact name overrides every file (for CI),
and each format engine substitutes inside values with its own escaping. Previews keep
placeholders and report each secret asresolved(with its source),MISSINGorunknown
(git-crypt locked); a new--reveal-secretsflag shows real values. A real run writes nothing if
any secret is unresolved.--listshows the chain's secrets files. Secrets files must end in
.secret.envand live inside.configtransform/, so the git-crypt rule always covers them, and
a name defined twice (in one file, or in two files of one layer) is an error. - Secrets, stage 2: whole-file secrets via a resource's
replace(docs/SECRETS_DESIGN.md). A
resource entry can name an encrypted*.secret.*file inside.configtransform/that replaces
the resource byte for byte — a Firebase service-account JSON, a certificate — with no merging or
parsing, so any format works, binary included and with no format engine needed. A later layer's
replaceoverrides an earlier one and supersedes earlier patches; a patch at or after the
replacing layer is an error, andsetrefuses to patch a replaced resource. Previews print a
one-line note unless--reveal-secrets; a still-encrypted replace file stops a real run before
anything is written.
Changed
- A
configtransform.jsonfield the tool doesn't recognize is now an error. Before, unknown
fields were silently skipped — so an older tool reading a layer that usessecretswould deploy
unresolved placeholders without a word. From now on a too-old tool fails loudly. A layer with a
stray or misspelled field will now fail to load; the error names the field. EnvFile(the.envgrammar) moved fromConfigTransform.EnvtoConfigTransform.Core, so
*.secret.envfiles and.envresources are parsed by the same code. No behavior change.
0.23.1-alpha
Fixed
- JSON and YAML output now keeps the base file's line endings and final newline. Found in
config-transform-pilot's CI log: merged JSON had no final newline, so the log'scatstep glued
the next line onto its closing}. Both engines also used the platform's newline, so on Windows
every JSON/YAML output was CRLF even for an LF source. Worse,setrewrote committed files the
same way: a base-fileseton JSON stripped the file's final newline and, on Windows, flipped
its line endings. Output now uses the base file's line endings and ends with a newline exactly
when the base does, as XML output always has (TextLayoutin Core). Every filesetrewrites
keeps its own conventions; a fileset/initcreates (overlay orconfigtransform.json) gets
LF and a final newline. The golden expected-output tests now compare byte for byte.
0.23.0-alpha
Added
--color auto|always|never(also--color=<mode>), for ANSI colour in every diff the tool
prints:--diff,--diff-layers, andset's automatic diff after a write. Accepted in every
mode.GitDiff.Renderalways passed--color=alwaysbefore this, so a redirect or pipe
(--diff > change.diff) captured raw escape codes. Seedocs/USAGE.md.
Changed
- JSON/YAML keys now match case-sensitively across layers, and a patch key that matches an
existing key only by case is an error. Repo owner's decision, following the tree merge below
(docs/TREE_MERGE_DESIGN.md's "Key matching is case-sensitive"). Before, a patch'sapiUrl
silently overrode the base'sApiUrl. That was wrong for case-sensitive consumers (a Python or
Node app sees two different keys), and plain case-sensitivity would instead deploy a file with
both keys, which .NET's configuration loader refuses to load. The merge now stops with an error
naming the patch file, the key path and the existing spelling.setchecks its--match key=
path the same way before writing (Try: --match key=Logging:LogLevel:Default), and now merges
the new overlay content before writing it, so a write the merge would reject leaves nothing on
disk. An existing overlay whose key casing differs from its base now fails to resolve — fix
the spelling the error names. - JSON and YAML now merge each patch into the base document's own tree, keeping the source's
key order, key spelling, and every value exactly as written (docs/TREE_MERGE_DESIGN.md).
Both engines used to flatten the base and patches throughMicrosoft.Extensions.Configuration
and rebuild a document from string keys. That sorted keys alphabetically, re-quoted YAML, and
corrupted values:"007"became7,"1.10"became1.1,"true"becametrue, a 20-digit
integer lost precision,nullbecame"",{}/[]were dropped, a patch's key spelling
replaced the base's, and a patch scalar landing on a base object was silently dropped.--diff
hid all of it, since it renders the unpatched side through the same merge. Kept on purpose:
IConfiguration's merge rules (key-by-key, arrays by index, the{"1": ...}index-keyed idiom)
and$elemMatchresolution; key matching became case-sensitive (entry above). Also changed: a later
layer now replaces a value of a different kind outright (the old merge always kept the object);
keys differing only by case within one file are allowed (YAML used to throw); an index-keyed
patch that would leave a gap in an array is an error; a multi-document YAML file is refused;
YAML output reuses the base file's indentation and sequence style. Comments are still not carried
into the output. Deployed JSON/YAML files can change wherever they relied on the old
reordering or type-guessing — compare before rolling out.Microsoft.Extensions.Configuration,
Microsoft.Extensions.Configuration.JsonandNetEscapades.Configuration.Yamlare no longer
dependencies. New golden expected-output fixtures (Fixtures/*/Expected/) pin the full output. - Diff output is no longer coloured by default when stdout isn't a terminal. The default,
--color auto, colours only on a terminal with theNO_COLORenvironment variable unset. A CI
log that renders ANSI (GitHub Actions does, even though a job's stdout isn't a terminal) now
gets plain text unless the step passes--color always.GitDiff.Rendernow passes
--color=neverexplicitly when colour is off, so a user's owncolor.ui = alwaysgit config
can't force it back on.
Fixed
-
A JSON patch file containing
//or/* */comments no longer crashes the merge. Comments
and trailing commas were accepted in a base file but not in a patch (the$elemMatchpre-scan
parsed patches strictly). Fixed by the tree merge above, which reads every file the same way. -
--diff-layersspaces a layer's second and later hunks with a blank line, the same as
between two layers' sections. Before, a layer's second[<layer>]tag followed the previous
hunk's last line with no gap. Seedocs/DIFF_LAYERS_DESIGN.mddecision log #5. -
JSON output keeps non-ASCII and
< > & ' +as written, instead of\uXXXXescapes.
Found during the same read-through as the console fix below. System.Text.Json's default encoder
wrote a passworda+basa\u002Bband a Hebrew value as a run of\u05XXescapes. This
affected--outputdeploy files too, not just console output. It was always valid JSON, but
unreadable in a deployed file or a--diff. Every JSON write site now uses
JavaScriptEncoder.UnsafeRelaxedJsonEscaping:JsonLayerMerger(both the plain and$elemMatch
paths),JsonFieldAuthor(set),JsonElemMatchResolver's ambiguous-match error message, and
LayerManifestSerializer(configtransform.jsonfiles written byset/init, where a client
namedAcme & Coused to come out asAcme \u0026 Co). New values in theDotNetCore,
GenericJsonandElemMatchfixtures cover it. Seedocs/CONFIG_MANAGEMENT.md§5.3, including the
one quirk left over from System.Text.Json itself (emoji are always written as escaped surrogate pairs). -
Console output is always UTF-8, so redirected output no longer loses characters on
Windows. Found while running the tool againstconfig-transform-pilotfrom Git Bash: on a
console code page like 437 (the Windows default for cmd.exe and Git Bash), .NET encoded
stdout/stderr in that code page. The resolution report's↓came out as the invisible control
byte0x19, and a non-ASCII config value printed by--dry-run(e.g. Hebrew) became?, which
is real data loss for--dry-run > file.--outputfiles were never affected. The new
Utf8Console(ConfigTransform.Cli) writes redirected output as UTF-8 with no BOM. On a real
console it switches the code page to UTF-8 for the run and restores the original on exit. See
docs/CONFIG_MANAGEMENT.md§6. Regression testUtf8ConsoleTestsruns the real CLI as a child
process with redirected stdout; an in-processCliRunner.Runtest with aStringWritercan't
reach this code.
0.22.0-alpha
Added
--diff-layers:--diff's per-layer sibling. Raised by the repo owner while reading a real
multi-hop--diff: there was no way to tell which layer (Environment, Client, or one specific
Host) produced a given changed line without separately running--listand reasoning it out by
hand.--diff-layersprints one diff per layer that actually changes the resource instead of one
diff comparing the base file straight to the final merged result — computed by re-running the
same merge with one more patch applied each time, needing zero changes to any of the four format
engines sinceLayerMergealready accepts an arbitrary prefix of the ordered patch list. Each
section is tagged[<layer>], or[<layer> overrides <earlier layer>]when every line it
changes was last touched by that one earlier layer; a hunk that re-touches lines with different
prior owners gets a plain[<layer>]tag instead, with a(overrides <layer>)note on each
individual changed line that has one (LayerDiffAttribution, reusingGitDiff.Render's own
unified-diff hunk headers for line-position bookkeeping rather than a second diff engine).
Mutually exclusive with--diff. Seedocs/DIFF_LAYERS_DESIGN.mdfor the full design and status.
0.19.0-alpha
Added
- A third, optional layer axis:
--host/-H(docs/HOST_LAYER_DESIGN.md) — raised by the
repo owner from a real deployment shape: a load-balanced Production environment where
individual servers need genuinely different config from each other, not just from other
clients/environments. Rejects the hyphenatedClient-Hostnaming workaround (e.g.
Acme-192.168.10.10) in favor of one more optionalextendshop,
.configtransform/Clients/<C>/<E>/Hosts/<H>/configtransform.json, reusing the
self-describing-layers chaining as-is.LayerPathResolver.Resolvegains an optionalhost
parameter (requiresclient+environment, mirroring the existingclient-requires-
environmentrule one level up); applies uniformly to a plain resolve,--dry-run/--diff,
--list(both modes), andset.setinto a new Host layer defaults itsextendsto the
matching Client/Environment layer, the same convention every other layer's default already
follows, one level deeper (SetTargetResolver).initgains a repeatable--hostflag
(quiet mode) and one more interactive prompt (asked only once a client was given), cross-
multiplying with every declared client × environment pair the same way clients already
cross-multiply with environments (InitPlanner), plus a matching case-insensitive-collision
check.-H(capital) is the short alias —-hstays--help, a deliberate, documented
tradeoff (docs/HOST_LAYER_DESIGN.md's decision log #2). Verified to need zero changes, by
reading the actual code rather than assuming:LayerChain'sextends-chain walk and
--list --resource's reverse lookup already treat directory depth as arbitrary, and every
format engine'sMergemethod only ever sees an ordered patch-path list, never a layer's
position in the tree. 31 new tests acrossConfigTransform.Core.Tests/
ConfigTransform.Cli.Tests.init --template's own--host-aware variant
(docs/HOST_LAYER_DESIGN.mddecision log #7) is deliberately deferred to a separate PR — the
fixed canned tree stays the minimal, no-decisions starter it's always been.