Skip to content

fix(CI): read Vault secrets by field-agnostic import in Windows smoke job - #3446

Open
Cynthia Qin (cqin-confluent) wants to merge 4 commits into
mainfrom
fix-smoke-windows-vault
Open

fix(CI): read Vault secrets by field-agnostic import in Windows smoke job#3446
Cynthia Qin (cqin-confluent) wants to merge 4 commits into
mainfrom
fix-smoke-windows-vault

Conversation

@cqin-confluent

@cqin-confluent Cynthia Qin (cqin-confluent) commented Aug 14, 2026

Copy link
Copy Markdown
Member

Release Notes

No user-facing changes. This PR touches CI pipeline configuration only.

Checklist

  • I have successfully built and used a custom CLI binary, without linter issues from this PR. — N/A, no Go code changed
  • I have clearly specified in the What section below whether this PR applies to Confluent Cloud, Confluent Platform, or both.
  • I have verified this PR in Confluent Cloud pre-prod or production environment, if applicable. — N/A, CI-only change
  • I have verified this PR in Confluent Platform on-premises environment, if applicable. — N/A, CI-only change
  • I have attached verification results in the Test & Review section below — including an explicit statement of what could not be verified.
  • I have added appropriate CLI integration or unit tests for any new or updated commands and functionality. — N/A, no commands changed
  • I confirm that this PR introduces no breaking changes or backward compatibility issues.
  • I have indicated the potential customer impact if something goes wrong in the Blast Radius section below.
  • I have put checkmarks below confirming that the feature associated with this PR is enabled in: — N/A, no feature
    • Confluent Cloud prod
    • Confluent Cloud stag
    • Confluent Platform
    • Check this box if the feature is enabled for certain organizations only

What

Applies to neither Cloud nor Platform — this changes .semaphore/smoke-tests.yml only, no shipped CLI code.

The Smoke windows/amd64 job has failed on every run since it was introduced in #3342 on 2026-05-01 — roughly 3.5 months at a 3-hourly cadence, each failure firing a Slack notification. It always dies on the same line:

Field "CONFLUENT_CLOUD_EMAIL" not present in secret

Root cause

These CI secrets do not store one field per credential. They keep everything in a single field named script, whose value is a shell script of export NAME=value lines. The helper the linux/arm64 job uses requests that one field and sources it; it performs no mapping or renaming of variable names, and the vault helper it calls beforehand only handles OIDC auth.

So there is no CONFLUENT_CLOUD_EMAIL field at all — which is exactly what Vault reported. The Windows job hand-rolled the read with vault kv get -field=<VARIABLE NAME> because the helper is Linux-only, and every lookup missed.

Fix

Read the script field and parse the assignments out of it, since PowerShell cannot source a shell script. The parser tolerates export or bare assignments, surrounding single or double quotes, values containing =, and skips comments and blank lines.

Only the variables the job needs are exported. Deliberately not "export everything the script defines": that would put unrelated values into the environment inherited by go test, the CLI under test, and the metric emitter, for no benefit.

This is a CLI-side bug, not a Vault or DevOps problem

Worth stating, since "Vault error" reads like someone else's issue:

Evidence What it rules out
vault login exits 0 on the Windows agent Auth, OIDC role, network reachability, token issuance
Error is Field ... not present in secret The read succeeded. Permission errors say permission denied; bad paths say No value found at ...
linux/arm64 reads the same secret path and passes The secret exists, holds valid credentials, and the CI role can read it
These lines were never modified after the commit that added them No previously-working state exists, so no Vault-side change broke it

The job was merged having never passed once. Nothing needs to change in Vault or in DevOps tooling to fix it.

Blast Radius

None for customers. No shipped CLI code paths are modified.

CI risk is close to zero because the job fails 100% of the time today — there is no working behavior to regress. The worst case is that it keeps failing, which is the status quo. The linux/arm64 block is byte-for-byte untouched, so existing smoke coverage is unaffected either way.

The one CI-level risk worth naming is a malformed pipeline definition, which could take the passing Linux job down with it. Mitigated: the YAML parses, and the - | multi-line PowerShell construct is already used twice in this file, including in this same Windows job.

No false-green risk: a missing or empty variable throws at import, and requiredEnv in the live tests fatals on an empty value. The job cannot pass without real credentials.

Security notes

  • No change to what is read from Vault. The same secret paths are read with the same token and the same read-only operation. Only what reaches the environment changes, and it is limited to the three variables the job needs.
  • The exit code is checked before the output is used, so a partial or malformed response cannot end up echoed through a downstream parse error.
  • No values are ever echoed. The success path prints nothing. The only error paths emit a vault path, a variable name, and — on a missing variable — the list of variable names the script defines.
  • Nothing sensitive is added to this public repo. The diff contains only vault paths and variable names, all of which were already present in the file.

References

Test & Review

Verified:

  • The regex and quote-stripping semantics were run against representative source-able lines — export NAME=value, double- and single-quoted values, a quoted value containing =, a bare assignment with no export, extra whitespace, and comment/blank/garbage lines. All parsed or skipped as intended, and an empty value is caught by the guard. To be precise about the limitation: this was executed through an equivalent port, not in PowerShell, because no PowerShell is available on the authoring machine.
  • YAML parses, the new PowerShell survives as exactly one command block rather than being split, the # lines inside it are preserved as PowerShell comments rather than stripped as YAML comments, and braces and parentheses balance.
  • The linux/arm64 block is byte-for-byte untouched.
  • Reviewed by inspection: $LASTEXITCODE reflects vault as a direct native-command assignment (no intervening pipeline), and [Environment]::SetEnvironmentVariable with two arguments uses Process scope — the same scope the existing $Env:... lines in this job already rely on to persist across commands.
  • The parse loop deliberately uses a positive -match rather than testing -notmatch and then reading $Matches. The latter does work, but it relies on non-obvious operator behavior that cannot be checked without PowerShell, so the dependency was removed.
  • Multi-line native-command output is captured by PowerShell as a string array; splitting on \r?\n is correct for both that and the single-string case, and .Trim() removes any stray \r.

Not verified: there is no PowerShell, no Windows host, and no Vault access on the machine this was written on, so it has not been executed. The smoke pipeline is also scheduled against main only, so PR CI will not exercise it — the first real signal is the next scheduled run after merge.

The remaining unknown is narrow: the exact shell syntax inside the script field. The parser is deliberately tolerant of the plausible forms, and if it still cannot find a variable it fails immediately and lists the names the script does define, so one run would pin it down.

Revision history of this PR

The first two commits diagnosed this incorrectly — they assumed one field per credential stored under a different casing, and matched case-insensitively. That would not have fixed the job. The third commit corrects the diagnosis after checking how the Linux helper actually behaves; the fourth is review cleanup with no intended behavior change. Reviewers should read the final state rather than the intermediate commits.

🤖 Generated with Claude Code

… job

The Smoke windows/amd64 job has failed on every run since it was added in
#3342 on 2026-05-01, always at the same line:

    Field "CONFLUENT_CLOUD_EMAIL" not present in secret

The job authenticates fine (`vault login` exits 0) and the read itself
succeeds, so this is neither a permissions nor a path problem. The
linux/arm64 job reads the same secret and passes, which proves the secret
exists and holds valid credentials.

The difference is how each job reads it. Linux uses `vault-sem-get-secret`,
which exports every field the secret contains, so it is agnostic to how those
fields are named. Windows cannot use that helper (it is Linux-only), so the
read was hand-rolled with `vault kv get -field=<NAME>` using the *exported*
variable names. Those are not the names the fields are stored under -- the
helper is what maps them -- so every lookup missed.

Mirror the helper instead of hardcoding names: read each secret whole and
export every field as an uppercased variable. This removes the coupling to
field naming, so it also will not break the next time these secrets are
rotated or renamed.

Two supporting changes:

- Fail immediately if an expected variable is still unset after the import,
  rather than 20 minutes later inside `go test` as "required environment
  variable ... is not set".
- Log the field names that were imported. Values are never echoed. If the
  stored names turn out to differ by more than case, this makes the very next
  run report exactly what they are.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@cqin-confluent
Cynthia Qin (cqin-confluent) requested a review from a team as a code owner August 14, 2026 19:39
Copilot AI lite review requested due to automatic review settings August 14, 2026 19:39
@confluent-cla-assistant

Copy link
Copy Markdown

🎉 All Contributor License Agreements have been signed. Ready to merge.
Please push an empty commit if you would like to re-run the checks to verify CLA status for all contributors.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR updates the Semaphore smoke-test pipeline to make the Windows/amd64 job read Vault secrets in a field-agnostic way (mirroring vault-sem-get-secret used on Linux), eliminating the current hardcoded -field=... lookups that have caused the Windows smoke job to fail consistently.

Changes:

  • Replace per-field Vault reads on Windows with a PowerShell helper that imports all secret fields and exports them as uppercased environment variables.
  • Add a fail-fast check to error immediately if required variables still aren’t present after import.
  • Add logging of imported field names only (no secret values) to aid debugging.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread .semaphore/smoke-tests.yml Outdated
Comment on lines +74 to +85
function Import-VaultSecret($Path) {
$secret = vault kv get -format=json $Path | ConvertFrom-Json
if ($LASTEXITCODE -ne 0) { throw "failed to read Vault secret $Path" }

# KV v2 nests fields under .data.data; KV v1 puts them directly on .data.
$fields = if ($secret.data.PSObject.Properties.Name -contains "data") { $secret.data.data } else { $secret.data }

foreach ($field in $fields.PSObject.Properties) {
[Environment]::SetEnvironmentVariable($field.Name.ToUpper(), $field.Value)
}
Write-Host "Imported $Path fields: $(($fields.PSObject.Properties.Name | Sort-Object) -join ', ')"
}
Two hardening changes from a review of the previous commit.

Check vault's exit code before piping to ConvertFrom-Json. On a partial or
malformed response the parse would run first, and PowerShell's JSON errors can
echo the offending input, which may contain secret values.

Export only the three variables the job needs, matched case-insensitively,
instead of exporting every field in both secrets. The previous approach
mirrored `vault-sem-get-secret`, but it also placed every unrelated field in
those secrets into the environment inherited by `go test`, the CLI under test,
and the metric emitter -- a wider disclosure surface than the job requires.
Matching case-insensitively keeps the fix independent of how the fields are
cased, which was the point of reading them by hand.

Note this does not change what is read from Vault: `vault kv get -field=` has
no server-side projection, so the whole secret was already fetched either way.
Only what reaches the environment changes.

The explicit post-import guard is now redundant and has been dropped, since a
missing or empty field throws at the point of export. On a mismatch the error
lists the available field names, so a single run still reveals them. Values are
never echoed, and the success path prints nothing at all.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The previous two commits diagnosed this wrong. They assumed the secret stored
each credential in its own field under a different casing, and matched fields
case-insensitively. That would not have fixed the job.

Checking how the linux/arm64 job's helper actually behaves settles it. It
requests one specific field, named "script", and sources its contents as a
shell script. It performs no mapping or renaming of variable names, and the
vault helper it calls beforehand only handles OIDC auth.

So these secrets keep everything in that single "script" field, whose value is
a shell script of `export NAME=value` lines. No CONFLUENT_CLOUD_EMAIL field
exists in any casing, which is precisely what Vault reported.

Read the "script" field and parse the assignments out of it, since PowerShell
cannot source a shell script. The parser tolerates `export` or bare
assignments, surrounding single or double quotes, values containing `=`, and
skips comments and blank lines.

The properties from the previous commit are kept: the exit code is checked
before the output is used, only the variables the job needs are exported, no
values are ever echoed, and a missing or empty variable throws immediately
while listing the names the script does define.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two cleanups from re-reviewing the whole change; no behavior change intended.

The parse loop tested `-notmatch` and then read `$Matches` after the negated
test. PowerShell does populate `$Matches` whenever the underlying regex matches,
regardless of which operator was used, so this worked -- but it depends on
non-obvious behavior that cannot be verified from a machine without PowerShell.
Use a positive `-match` and nest the body instead, which needs no such
assumption and reads more directly.

Rename `$script` to `$exports`. `$script` is legal but sits one colon away from
PowerShell's `$script:` scope modifier, which is needlessly confusing here.

Also note in a comment that the hashtable lookups are case-insensitive, since
PowerShell hashtables are by default. That means the import still succeeds if
the script declares these names under a different case -- worth stating, as an
earlier revision of this branch wrongly believed case was the whole problem.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@sonarqube-confluent

Copy link
Copy Markdown

Quality Gate passed Quality Gate passed

Issues
0 New issues
0 Fixed issues
0 Accepted issues

Measures
0 Security Hotspots
No data about Coverage
No data about Duplication

See analysis details on SonarQube

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants