Combined maintenance and release branches
I think you have similar requirements, like our people. CI will help you automate, but it will not solve the fundamental organizational task.
So, you have 2 types of checks:
- normal non-urgent code (planned for release every once in a while)
- urgent "correction" of the code (occurs after release, if something slips without sufficient testing or when client X calls because he wanted the button pink to not purple and he threatens to cancel the contract: P)
Both cases are different and you need to separate them. Your approach is already close to the answer, which I think, but you are doing "too much"
Let me describe what we do:
Repository Layout
trunk (is our project with components and all) branches | -- 1.0-stable-week-40 | -- 2.0-stable-week-42 | -- 3.0-stable-week-44 tags | -- 1.0.0 | -- 1.0.1 | -- 1.0.2 | -- 2.0.0 | -- 2.0.1 | -- 3.0.0
As you can see, we have a trunk for all major developments. We also create stable branches for preparing and testing releases every 2 weeks, and we mark all releases when they are released live.
Release Life Cycle (Generalized)
After the new version, we maintain the branch (e.g. 1.0) until the next major release is pushed out. Our policy is that during this time ONLY critical fixes can be checked in this thread. They pass only minimal testing and can be released in minutes by creating a new tag from our service branch.
Halfway through the maintenance period (1 week after release) we create a new branch from our trunk called "2.0". Everything is not so urgent development, already in the trunk, will be in this release automatically. Other things can be added “carefully”, for example, urgent corrections that come from the current active service branch (merge from 1.0 to 2.0 to the trunk).
After one more week, and all the tests were completed, the 2.0 branch got the 2.0.0 mark and was released, since there were no serious problems. The service branch 1.0 opens, and it will be deleted in the end.
Thus, we can separate urgent from non-urgent changes and have relatively painless and stable releases. What you do is almost the same, but you separate from the tag, which when you finish your tag again. That's not a lot:). Branching out tags is also bad. branch of branches.

branch policies
This helps the team if you write out a policy for each of your types of branches, allowing them to do more on their own, without the graduate guy constantly sitting on his neck and luring their commits;)
Our policies can be described as follows:
Trunk:
- not fixed with syntax errors
- gets service mergers
- no direct releases from this thread
branches / xx stable
- can only accept urgent corrections
- must be ready for release at any time
- the developer SHOULD merge his commit from here to any younger stable branch
- If there is no younger stable branch, merge with the trunk
tags / *
- Not fixed here
- used for deployment
Merging also becomes less mind when you try to merge in only one “direction”. (You might want to use Google for tofu, but now get some OT). See that mergers are performed continuously by developers, not release managers, to limit the bottleneck. Although one release is active and actively supported by patches, we are all starting to prepare the next one, isolating it from possible unstable changes in the trunk and giving it time to “mature”.
You may have different requirements for the duration of code incubation and testing, or the duration of release iterations, of course. Adapt:)