Surfaced by
Core-dump C-1 staging-PG cutover (workflow PR #538 / W-1..W-9 conformance, downstream consumer).
Behavior
R-A4 (cmd/wfctl/infra_align_rules.go::checkRA4) checks every `${VAR}` reference in module env_vars against `ctx.secretKeys` then falls back to `os.Getenv`. `ctx.secretKeys` is populated only from MODULE-form `secrets.generate` (per buildAlignContext switch arm at line 58). The TOP-LEVEL `secrets:` block populates `ctx.secretGens` (used by R-A9) but NOT `ctx.secretKeys`.
Effect
A config that declares secrets the canonical way (top-level `secrets:` block, e.g. core-dump's infra.yaml line 26-71 with `STAGING_PG_PASSWORD` as random_hex and `STAGING_VPC_UUID` as infra_output) will trigger R-A4 strict FAIL on `${STAGING_PG_PASSWORD}` references in container env_vars unless the GH workflow also exports the secret as an env var to the align step. This forces:
```yaml
- name: Validate alignment
run: wfctl infra align --strict
env:
STAGING_PG_PASSWORD: ${{ secrets.STAGING_PG_PASSWORD }} # workaround
```
even though W-5 JIT secret resolution at apply time means the value is never needed at align/plan time.
Expected
`buildAlignContext` should populate `ctx.secretKeys` with the keys from `cfg.Secrets.Generate` (and `cfg.Secrets.Requires` if the latter exists) at the same time it populates `ctx.secretGens`. R-A4 then correctly skips secrets declared in either form.
Test
```go
// cmd/wfctl/infra_align_test.go (new test)
func TestInfraAlign_RA4_TopLevelSecretsBlock_NoFinding(t *testing.T) {
yaml := `
secrets:
generate:
- key: DB_PASSWORD
type: random_hex
length: 32
modules:
- name: api
type: infra.container_service
config:
image: "myapp:latest"
env_vars:
DB_PASS: "${DB_PASSWORD}"
`
// expect zero R-A4 findings
}
```
References
- Discovery: core-dump deploy.yml C-1 cutover; `feat/c1-staging-pg-cutover` branch
- W-5 design: `iac/jitsubst/jitsubst.go` (JIT resolution makes plan-time secret-value substitution unnecessary)
- Related: R-A9 (cmd/wfctl/infra_align_rules.go:683) DOES use top-level `ctx.secretGens` correctly — R-A4 should follow the same pattern
Surfaced by
Core-dump C-1 staging-PG cutover (workflow PR #538 / W-1..W-9 conformance, downstream consumer).
Behavior
R-A4(cmd/wfctl/infra_align_rules.go::checkRA4) checks every `${VAR}` reference in module env_vars against `ctx.secretKeys` then falls back to `os.Getenv`. `ctx.secretKeys` is populated only from MODULE-form `secrets.generate` (per buildAlignContext switch arm at line 58). The TOP-LEVEL `secrets:` block populates `ctx.secretGens` (used by R-A9) but NOT `ctx.secretKeys`.Effect
A config that declares secrets the canonical way (top-level `secrets:` block, e.g. core-dump's infra.yaml line 26-71 with `STAGING_PG_PASSWORD` as random_hex and `STAGING_VPC_UUID` as infra_output) will trigger R-A4 strict FAIL on `${STAGING_PG_PASSWORD}` references in container env_vars unless the GH workflow also exports the secret as an env var to the align step. This forces:
```yaml
run: wfctl infra align --strict
env:
STAGING_PG_PASSWORD: ${{ secrets.STAGING_PG_PASSWORD }} # workaround
```
even though W-5 JIT secret resolution at apply time means the value is never needed at align/plan time.
Expected
`buildAlignContext` should populate `ctx.secretKeys` with the keys from `cfg.Secrets.Generate` (and `cfg.Secrets.Requires` if the latter exists) at the same time it populates `ctx.secretGens`. R-A4 then correctly skips secrets declared in either form.
Test
```go
// cmd/wfctl/infra_align_test.go (new test)
func TestInfraAlign_RA4_TopLevelSecretsBlock_NoFinding(t *testing.T) {
yaml := `
secrets:
generate:
- key: DB_PASSWORD
type: random_hex
length: 32
modules:
type: infra.container_service
config:
image: "myapp:latest"
env_vars:
DB_PASS: "${DB_PASSWORD}"
`
// expect zero R-A4 findings
}
```
References