In my previous work, we used a similar approach to what @Alexis mentioned, with one major difference. Keep in mind that we were working on a new version of some fairly large software (our code base was several million lines of code, between Java, flex and COBOL), and we had beta testing for partners. The releases were two- / weekly, including for the client (although they would usually be one release after the last, since he would first go through QA), and this week we had to perform a “cut”, test, basic QA a client of our code, who was another developer in the company, and then released a real QA.
In fact, the master was our dev branch. If the dev element should have taken more than one or two days, it was completed on the function branch, and then merged into dev when it was ready. There was another “future” branch of dev, which was reserved for fairly serious work with new functions (which significantly changed the program) or major refactoring. At some point, this will become the main “dev” when we decide that we have time to properly test and fix bugs, or that it is time to introduce new features and deal with the inevitable pain :)
When the time came for the release, a new branch was created with the name "release_x", then all the corrections that came from QA were implemented there and merged "up". By this, I mean that we can have two or three versions in the game at any given time, so the client will obviously have the oldest one that we could fix if they found showstopper. This will be done in the patch branch coming out of the corresponding version, which at some point will be merged with this version and removed (so that you can easily see outstanding corrections in the list of branches) and another assembly executed and sent to the client. The “fixes” sections existed so that we could select and select what fell into a particular assembly, which functioned both for the client release and for the developer release, in order to avoid potentially risky fixes for small problems that violate the release of the fix for showstopper.
Then it will be combined with the release that the QA guys had, then it will be combined with the release that other developers used (always the latest version due to their dependence on our plugins and the j2ee infrastructure to do their work), then back to dev. just to keep everything up to standard.
All releases that currently play the game have their own automatic build loop in Jenkins, as well as the dev branch, with some automated functional tests running on the more important ones (dev and QA mostly). Each assembly added a tag to the commit report, which was used as HEAD, and the assembly number was available from the program so that we could see exactly which version the error reporter used.
In fact, there are two dev branches, one of which for major work will be released later as a new major version. Unlock branches for each version, with patch branches resetting from them. Finding the latest version was no easy task; look for the release branch with the highest number.
The only drawback was that if you made many corrections when releasing several versions back, then the merging of the charts was ... interesting to perform :)