The rule is not “always commit the lock file” — it depends on what the package is. An application is deployed, so it wants the exact versions that were tested. A library is installed into someone else’s project, where its own lock file is ignored entirely.
# application: lock committed, deploy installs exactly what was tested
composer install
# library: lock is not committed; the consumer resolves against their tree
composer update
The consequence people get wrong is that composer install on an application should be the only command a deploy ever runs. If the deploy script says update, it resolves fresh versions on the production machine, and the code that ships is not the code that passed CI. Committing the lock is what makes the two identical.