I'm using a combination of GitHub Actions and Forge and Envoyer:
When opening a new PR a GitHub action will run the tests on each commit to the branch of the PR. When tests are green there's another step that merges the branch of the PR into a staging branch. On my Forge server I enabled quick deploy for this staging branch, so whenever I open a PR or commit to it, my tests run and when they're successful the code gets shipped to my staging env.
Now when I merge the PR into main, Envoyer will pickup and do the rollout to my production env with zero downtime. It's of course a matter of taste if you want to deploy directly when something got merged into main, but until now this approach took quite far down the road :)