#24306·pulumi

Fix partially-installed policy packs being treated as installed forever

Author: smithrobsCreated Aug 13, 2026Updated Sep 16, 2026
Labelskind/bugarea/policy-as-codepulumi/pulumi

Problem

If a required policy pack's installation fails partway through, the CLI treats the pack as installed on every subsequent run and never re-downloads it. Every later update fails with a confusing error that points at the symptom rather than the cause:

error: failed to load Pulumi policy project located at "/Users/me/.pulumi/policies/myorg/pulumi-analyzer-hitrust-google-cloud-v2.1.1": open /Users/me/.pulumi/policies/myorg/pulumi-analyzer-hitrust-google-cloud-v2.1.1/PulumiPolicy.yaml: no such file or directory

The pack directory on disk existed but contained only an empty node_modules — no PulumiPolicy.yaml, no package.json. There is no way to recover other than manually rm -rf-ing the pack directory, which requires knowing the on-disk layout.

Two distinct defects combine to produce this:

  1. The installed-check is too weak. GetPolicyPath (sdk/go/common/workspace/plugins.go:2001) reports installed = true if the pack path exists and is a directory. It never checks that the directory holds a usable policy pack. That result flows through cloudRequiredPolicy.Installed() (pkg/backend/httpstate/policypack.go:92) to installPolicyPack (pkg/engine/policypacks.go:44), which returns early and skips download and extraction. The analyzer is then loaded from that same directory and fails at pkg/resource/plugin/analyzer_plugin.go:141.

  2. Failed installs leave the directory behind. In installRequiredPolicy (pkg/backend/httpstate/policypack.go:499) the defer only removes tempDir. Once os.Rename(tempPackageDir, finalDir) succeeds at line 539, any later failure — the LoadPolicyPack at line 544, or InstallDependencies at line 567 — returns an error while leaving finalDir in place. Combined with defect 1, that poisons the cache permanently: the run that actually failed reports the real install error, and every run after it reports the misleading load error above.

Proposed Solution

  • Make GetPolicyPath report installed = true only when the directory contains a PulumiPolicy.yaml. A directory without one is a partial install, not an installed pack.
  • Make installRequiredPolicy clean up finalDir when it fails after the rename, so a failed install leaves no trace and the next run retries from scratch.

Implementation Notes

  • GetPolicyPath is in the sdk module and is also used by Installed() / LocalPath(); it must keep returning the computed path alongside the boolean, since callers rely on the path even when the pack is not installed.
  • Cleanup in installRequiredPolicy should not fire on the benign concurrent-install case already handled at line 539 (os.IsExist(err), which covers ENOTEMPTY), where another process legitimately owns finalDir.
  • Both changes are needed. Fixing only the cleanup still leaves already-broken directories on users' machines wedged; fixing only the check means every failed install re-downloads but never reports why.

Acceptance Criteria

  • Given a pack directory that exists but has no PulumiPolicy.yaml, when an update runs, then the pack is re-downloaded and installed rather than reported as installed
  • Given a pack whose dependency install fails, when the install returns, then no pack directory is left behind and the error names the dependency failure
  • Given a pack whose dependency install fails, when a second update runs, then it retries the install and surfaces the install error, not a PulumiPolicy.yaml not-found error
  • Given a fully installed pack, when an update runs, then it is still treated as installed and is not re-downloaded
  • Tests added covering each scenario above

Scope

In scope: the installed-check in GetPolicyPath and failure cleanup in installRequiredPolicy.

Out of scope: validating pack contents beyond the presence of PulumiPolicy.yaml; the analogous install/cleanup paths for resource provider plugins; any change to the error text at pkg/resource/plugin/analyzer_plugin.go:141.