What is the best way to model development, uat/staging and production environments for infrastructure code in Github

This question is particularly about infrastructure and/or provisioning code. E.g. Chef, Terraform.
Let’s say we have an infrastructure component we want to deploy in following environments, development, uat and production. The recommendation from the “infrastructure as code” products is to maintain different branches with the environment names (dev, uat, prod/master, etc) in the repo. And a merge into any of those branches could trigger a deploy to the corresponding environment.

We tried to do this with our chef-repo (with branches dev and prod). The problem was to keep both the branches in sync with each other. Merge types “Squash and merge” or “Create a merge commit” obviously create a different commit tree in the destination branch.
i.e. When you merge commits aaa, bbb and ccc from dev -> prod,

  • with squash and merge, they become yyy
  • with merge commit, they become aaa, bbb, ccc, zzz last one is the merge commit

In both merge types stated above, the commit trees of dev and prod branches have diverged.
So, later when you merge some more commits from dev->prod, it will also duplicate the commits from the last merge (basically all the commits since the branches diverged, because even if the code is the same, the commit SHA isn’t)

So, we restricted our merges only to “Rebase and merge” method. However, this method also changes the committer information at times and the commit hashes in the destination branch are different from those of the source branch. So we end up in the same problem.

In the end we added one more step after every merge from dev->prod. We do a git pull -r origin prod && git push origin dev -f on the dev branch. This gets both the branches in sync. And things have been working as expected.
Only problem with this approach is, this is a manual step after every single PR merge which needs to be communicated and reasoned to every new person who starts to contribute to this repo. And the force push somehow makes me feel that I’m not following the user manual.

The Github flow suggests to keep only one deployable branch at any given time. And create short lived branches as needed from master. But for infrastructure code IMO there will be multiple branches deployable to different environments, which will need to be in sync with each other.

Could anyone provide a Github workflow that works for infrastructure code ? I’d be very interested in trying it out.