We had to solve the same problem. We have come to the sollowing solution that works pretty well.
All code into master must be through a pull request. To complete a pull request it must build and be approved by an required reviewer. It is important that the system is setup to build a temporary merge commit from the branch and master(similar to the commit if the PR were to be completed)
After the build is completed a release is created with the artifacts from the build. This release is deployed to dev servers. Each deploy gets a port number where we can reach this specific version of the webapp.
Lets say we have the following environments. DEV,RUNTESTS,UAT,PROD. After deploy to dev RUNTESTS env is run for this particular release. If everything is good the required reviewer gives the PR an approve and it can be completed.
This ensures it is very hard to put broken code into master. Another benefit is that we get a port number for a specific release that can be used for demo purposes(PO, stakeholders, analysts...)
Each morning a build from master is scheduled and deployed to DEV and RUNTESTS and we can then choose to put it into UAT and PROD if we like. Since we have the temporary release BEFORE the code hits master we don't need to trigger a build on each commit.