In addition to nix and guix, which have been mentioned, you can also use straight.el[0]. It's a functional package manager for emacs, so you'd use it instead of package.el. It checks out the git repo for each package and maps package name->commit hash in a nice little file, which you can then commit to git. Then, on a new machine, it will check out that specific rev for all your packages.
It even has nice use-package integration, so if you're already using that you likely won't have to change all your config.