Fix a proven OpenTofu-vs-pulumi-hcl divergence and ship it as its own PR. Use after find-tfcompat-bug has produced a failing tfcompat test — fix the runtime to match OpenTofu, sweep for the sibling bug, add unit coverage, and open a PR. Use when asked to fix/ship a known tfcompat mismatch.
npx skills add https://github.com/pulumi/pulumi-hcl --skill fix-tfcompat-bug
This skill starts from a proven failing tfcompat test — a divergence already found and
captured by the find-tfcompat-bug skill, with a
tests/tfcompat/<l1|l2>_<name>_test.go + testdata/cases/<name>/ fixture that **fails on
master for the right reason** (a genuine OpenTofu-vs-pulumi diff or matched ExpectErr,
not a harness error). Edit the pulumi-hcl runtime to faithfully match OpenTofu, verify
failing-before / passing-after, sweep for the sibling bug, and ship it as its own PR.
If you do not yet have such a failing test, stop and run find-tfcompat-bug first —
do not invent a fix without a proven failure.
This skill ships exactly one bug fix on its own branch off master with its own
changelog entry and PR. Do not stack fixes.
against OpenTofu source, the cty stdlib source, the docs, or an empirical tofu probe.
git add specific files only, never git add .. Don't touch staging the user left.shipping waits for the modeled flow below, which the user has standing-approved for
this loop — confirm if unsure).
The Go module cache root is !go env GOPATH/pkg/mod.
go env GOPATH/pkg/mod/github.com/pulumi/opentofu@*/lang/funcs/*.gogo env GOPATH/pkg/mod/github.com/zclconf/go-cty@*/cty/function/stdlib/pkg/hcl/ (expression eval, resource / module / provider handling)pkg/hcl/eval/functions.gopkg/hcl/eval/functions_test.goEdit the pulumi-hcl runtime to match OpenTofu. For a function, fix the Impl in
pkg/hcl/eval/functions.go — and prefer delegating to the cty stdlib.*Func when
OpenTofu itself binds that stdlib function (check the binding map); several bugs were
hand-rolled reimplementations that should have been stdlib.IndentFunc /
stdlib.FormatDateFunc. For an L2 bug the fix lives elsewhere under pkg/hcl/. Fix
minimally and faithfully; run go mod tidy if you add a dependency.
Add unit coverage next to the code you changed. For functions, add cases to
pkg/hcl/eval/functions_test.go; note evalExpr calls t.Fatalf on diagnostics, so
error-path cases must call <fn>.Call(...) directly rather than via evalExpr.
make build
PATH="$PWD/bin:$PATH" go test ./pkg/hcl/... -run '<UnitTest>' -count=1 -v
PATH="$PWD/bin:$PATH" go test ./tests/tfcompat/ -run 'Test(L1|L2)<Name>' -count=1 -v
Both must now PASS (with -count=1 — the rebuilt binary won't be picked up otherwise).
The tfcompat case that failed on master must now pass against your fix.
**Before you open the PR, check whether the same root cause produces a sibling
bug, and fix it in the same PR.** A divergence almost never lives alone: the same
helper, library choice, or code path usually backs a *sibling* operation, and
shipping only one half leaves the matching bug behind for the next migration to
hit.
Look in particular for:
unmarshal, get ↔ set. Fixing yamlencode (hand-rolled on yaml.v3 instead of
go-cty-yaml) left an *identical* yamldecode bug — same wrong library, mirror
symptom — that had to ship as a separate follow-up. Don't make that mistake:
when you fix one direction, probe the other in the same session.
helper or the same family (base64*, file*, cidr*, the to* converters).
Grep for other call sites of any helper you touched.
For each candidate, run a quick tofu probe against the equivalent pulumi-hcl
result. If it also diverges in the migration-affecting direction, fold the fix
into this PR and extend the one tfcompat case to cover both (one combined
l1_<family> case is fine — e.g. l1_yaml exercising both yamlencode and
yamldecode). If a sibling is genuinely out of scope, say so explicitly in the
PR body rather than leaving it silently unfixed.
Consult the submit-pr skill for opening a PR.
When writing your PR & changelog:
body terse — a short phrase naming the change from theuser's point of view (e.g. Coerce a variable default to its declared type
or Add base64gunzip, urldecode and cidrcontains ), not a sentence explaining
it, and with no trailing punctuation. Keep the <thing> (function/feature name)
in backticks.
OpenTofu", "instead of erroring", or other comparison framing). Parity with OpenTofu
is the whole project's premise, so stating it in the changelog is noise — just
describe what changed. The OpenTofu-vs-pulumi divergence narrative belongs in the **PR
description**, where you do say "OpenTofu" (never "Terraform").
code "matches OpenTofu" / "mirrors OpenTofu's X" — that applies to the entire codebase
and restates the premise. A comment should explain something the code itself doesn't
(e.g. *why* a helper is reimplemented rather than imported); if the only thing a
comment would say is "this matches OpenTofu," delete it.
Then, on its own branch off master (not stacked on a prior fix):
Report the divergence, the fix, the verification results, and the PR URL.
Take pulumi/fix-tfcompat-bug from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.