{"record":{"id":"84438e28dc9565a3","repo":"puppetlabs/puppet","slug":"puppet-plans-invalid-name","errorCode":"puppet.plans/invalid-name","errorMessage":"Plan names must start with a lowercase letter and be composed of only lowercase letters, numbers, and underscores","messagePattern":"Plan names must start with a lowercase letter and be composed of only lowercase letters, numbers, and underscores","errorType":"validation","errorClass":"Puppet::Module::Plan::InvalidName","httpStatus":null,"severity":"error","filePath":"lib/puppet/module/plan.rb","lineNumber":120,"sourceCode":"    def self.plans_in_module(pup_module)\n      # Search e.g. 'modules/<pup_module>/plans' for all plans\n      plan_files = Dir.glob(File.join(pup_module.plans_directory, '*'))\n                      .keep_if { |f| valid, _ = is_plans_filename?(f); valid }\n\n      plans = plan_files.group_by { |f| plan_name_from_path(f) }\n\n      plans.map do |plan, plan_filenames|\n        new_with_files(pup_module, plan, plan_filenames)\n      end\n    end\n\n    attr_reader :name, :module, :metadata_file\n\n    # file paths must be relative to the modules plan directory\n    def initialize(pup_module, plan_name, plan_files)\n      valid, reason = Puppet::Module::Plan.is_plans_filename?(plan_files.first)\n      unless valid\n        raise InvalidName.new(plan_name, reason)\n      end\n\n      name = plan_name == \"init\" ? pup_module.name : \"#{pup_module.name}::#{plan_name}\"\n\n      @module = pup_module\n      @name = name\n      @metadata_file = metadata_file\n      @plan_files = plan_files || []\n    end\n\n    def metadata\n      # Nothing to go here unless plans eventually support metadata.\n      @metadata ||= {}\n    end\n\n    def files\n      @files ||= self.class.find_files(@name, @plan_files)\n    end","sourceCodeStart":102,"sourceCodeEnd":138,"githubUrl":"https://github.com/puppetlabs/puppet/blob/e227c27540975c25aa22d533a52424a9d2fc886a/lib/puppet/module/plan.rb#L102-L138","documentation":"Puppet::Util::Plist.write_plist_file serializes a Ruby object graph to a plist via CFPropertyList and saves it to disk. The method rescues IOError, logs this Puppet.err with the inspected exception, and returns nil - callers get no exception, so a failed write is easy to miss unless logs are read or the file is stat'ed afterwards. It is the standard helper for Puppet's macOS plist writes. Because the rescue matches only IOError (and its subclass EOFError), Errno::* system call errors such as EACCES or ENOSPC are not caught here and propagate to the caller instead.","triggerScenarios":"CFPropertyList's save raising IOError while writing file_path (closed stream or a write failure surfaced as IOError); writing to paths under root-owned directories like /Library/Preferences from a non-root process when the error surfaces as IOError; a target whose parent directory is missing. Permission and disk-space errors raised as Errno::* bypass this handler entirely.","commonSituations":"macOS providers and custom facts that write preference plists under restricted privileges; writes toward SIP-protected paths; automation code that assumes write_plist_file raises on failure and never checks the result, so files silently go unwritten.","solutions":["Read the %{error} detail - e.inspect names the exact IOError class and message from the save.","Verify privileges and path: run as root for /Library/Preferences, and make sure the parent directory exists and is writable by the current user.","After every write_plist_file call, verify success explicitly (File.exist?, mtime, or re-read with read_plist_file) because the method swallows the failure and returns nil.","If you need real error propagation, bypass the wrapper: File.write(path, Puppet::Util::Plist.dump_plist(data)) - dump_plist does not rescue.","Keep the object graph limited to CFPropertyList-convertible types (String, Integer, true/false, Array, Hash, Time, binary data) so conversion cannot fail regardless of I/O."],"exampleFix":"# before: failure is swallowed - method returns nil, file may not exist\nPuppet::Util::Plist.write_plist_file(data, '/Library/Preferences/com.example.app.plist')\n\n# after: dump yourself and write atomically; errors now raise to the caller\nrequire 'fileutils'\ntmp = '/Library/Preferences/com.example.app.plist.tmp'\nFile.write(tmp, Puppet::Util::Plist.dump_plist(data))\nFileUtils.mv(tmp, '/Library/Preferences/com.example.app.plist')","handlingStrategy":"validation","validationCode":"# Pre-flight the write target before calling write_plist_file\ndef plist_target_writable?(file_path)\n  dir = File.dirname(File.expand_path(file_path))\n  File.directory?(dir) && File.writable?(dir) &&\n    (!File.exist?(file_path) || File.writable?(file_path))\nend\n\nfail 'plist target not writable' unless plist_target_writable?(path)\nPuppet::Util::Plist.write_plist_file(data, path)","typeGuard":null,"tryCatchPattern":"# The wrapper swallows IOError; propagate errors yourself with dump_plist\nbegin\n  File.write(path, Puppet::Util::Plist.dump_plist(data))\nrescue IOError, SystemCallError => e\n  raise \"plist write failed for #{path}: #{e.message}\"\nend","preventionTips":["Never trust write_plist_file's return value; verify the file changed (mtime or re-read with read_plist_file) afterwards.","Prefer dump_plist plus your own atomic File.write when you need failures raised.","Check directory existence and writability before system plist paths, and run as root for /Library/Preferences.","Remember the built-in rescue covers only IOError; Errno::* errors propagate - handle both in callers."],"tags":["puppet","macos","plist","cfpropertylist","file-io","silent-failure"],"backgroundTag":"plist-write-error","analyzedSha":"e227c27540975c25aa22d533a52424a9d2fc886a","analyzedAt":"2026-08-21T20:49:46.650Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}