The best way to ensure idempotency with exec is to invert your approach. Rather than focusing on how exec should invoke your script, focus on what this script is intended to accomplish. Test for that using the
Focusing on the test rather than the command to run is declarative approach to writing exec statements.
For example, if your script is responsible for managing security settings, start by writing
unless tests for those settings. Remember that you can pass an array to unless (all conditions must pass.) In this case, your test declares that the security settings are required, and execs the script to correct them if they are not set correctly.
If your script is responsible for installing an application, use an unless test that checks to see what version of the application is currently installed. If the application is not installed, or is the wrong version, the installer will then be invoked. In this case, your exec resource declares that a specific version of the application should be installed, and runs the installer if it's not correct.
I generally recommend refreshonly in cases where your script is actually refreshing something. Refreshonly can be unreliable; if the exec resource fails, there's no guarantee it will re-attempt the next time Puppet runs. If you do use refreshonly, be sure that a notification event will always be generated until the refresh succeeds.
I hope this helps.