That's what tags are for, QA tests the tagged release, then that gets released. Master can continue changing up until the next tag, then QA has another thing to test.
Presumably you are maintaining the ordering of these releases with your naming scheme for tags. For instance, using semver tags with your main release being v1.2.0 and your hotfix tag being v1.2.1, even while you've got features in flight for v1.3.0 or v1.4.0 or v2.0.0. Keeping track of the order of versions is part of semver's job.
Perhaps the distinction is that v1.2.0 and v1.2.1 are still separate releases. A bug fix is a different binary output (for compiled languages) and should have its own release tag. Even if you aren't using a compiled language but are using a lot of manual QA, different releases have different QA steps and tracking that with different version numbers is helpful there, too.
What are you trying to achieve here, or what's the crux? I'm not 100% sure, but it seems you're asking about how to apply a bug fix while QA is testing a tag, that you'd like to be a part of the eventual release, but not on top of other features? Or is about something else?
I think one misconception I can see already, is that tags don't belong to branches, they're on commits. If you have branch A and branch B, with branch B having one extra commit and that commit has tag A, once you merge branch B into branch A, the tag is still pointing to the same commit, and the tag has nothing to do with branches at all. Not that you'd use this workflow for QA/releases, but should at least get the point across.
It really depends on the whole development workflow, but in my experience it was always easier and less hassle to develop on the main/master branch and create stable release or fix branch as needed. With that one also prioritizes on fixing on master first and cherry-pick that fix then directly to the stable branch with potential adaptions relevant for the potential older code state there.
With branching of stable branches as needed the git history gets less messy and stays more linear, making it easier to follow and feels more like a "only pay for what you actually use" model.
And there it is. Not "potential adaptations", they will be a 100% necessity for some applications. There are industries outside webdev where the ideals of semver ("we do NOT break userland", "we do NOT break existing customer workflows", https://xkcd.com/1172/) are strongly applied and cherry-picking backports is not a simple process. Especially with the pace of development that TBD/develop-on-main usually implies, the "potential older code state" is a matter of fact, and eliding the backport process into "just cherry-pick it" as you did is simply not viable.
Sure, not all workflows are a good fit for this, that's why I started with a disclaimer. But, if you want to do stable release in general, then it's IMO better to adapt the general workflow to that (which provides other benefits too, like on bisecting or when writing actually good release notes), than trying to force that on a not so compatible workflow.
But I'd be happy to hear alternatives, like how you solve this; always interesting to read others perspectives that lay out their solutions (not just why mine doesn't work 1:1 for you, which is basically a given for any non-trivial project).
* GitFlow isn't perfect, but works for many, especially projects with lots of contributors working concurrently, and double especially if there are release gates that keep a human (release engineers) in the CI/CD loop for accountability, which is not uncommon in real world commercial projects. * GitHubFlow works for lots, but not all. Pretty good for moderate team sizes with some but not so much concurrent overlapping work, and perhaps a dedicated release engineer to handle those "potential adaptations". * TBD works for some, but can be inappropriate for many, especially when there are long-lived stable branches with commitments to ongoing security and bugfixes and the team size is smaller such that dedicated release engineers are not a thing.
This list is obviously not comprehensive, and just pulls from some workflows commonly presented elsewhere in this post.
Basically, I was just saying that one _can't_ just "[fix] on master first and cherry-pick that fix then directly to the stable branch with potential adaptions", because in most projects (non-trivial, as you said, which probably means anything with more than a couple contributors and an actual concept of a "stable branch") there will always be adaptations that need to be considered to maintain the stableness of that stable branch. And that's stableness from the user/customer/client PoV of "it acts the same but with less undefined behavior [read: bugs]", not just the dev PoV of "tests pass and it doesn't crash".
For smaller projects with tests, something like TBD is great: easy to reason about, branches are free, tags are great. For bigger things with many teams working on overlapping features, keeping a TBD setup "flowing" (pun intended) can require a bit more fore-thought and planning. Release engineering, in other words. TBD vs GF is kind of just "do you want your release engineering at the beginning or at the end"?
Then you can merge to master and it's immediately ready to go.
This also means that the release to prod happens post-integration by means of turning the feature flag on. Which is arguably a higher quality code review than pre-integration.
It sounds like you are doing a monorepo type thing. Git does work best and was designed for multiple/independent repos.
It makes using `git describe` a little bit more complicated, but not that much more complicated. You just need to `--match project-a/` or `--match project-b/` when you want `git describe` for a specific project.
For me, the scope of a tag should match the scope of the release. That means that a monorepo is only useful if the entire source tree is built and released at the same time. If you're using a monorepo but then do partial releases from a subtree, you're using the wrong solution: different repo's with a common core dependency would better match that workflow. The common core can either be built separately and imported as a library, or imported as a git submodule. But that's still miles ahead of any solution that muddles the developers' daily git operations.
I know there are monorepo tools out there that do things like automate partial releases include building the git tag names and helping you you get release trees, logs, and diffs when you need them.
I think a lot of monorepo work is using more domain specific release management tools on top of just git.
Also, yeah, my personal preference is to avoid monorepos, but I know a lot of teams like them and so I try my best to at least know the tools to getting what I can out of monorepos.
I found someone else's list of well known open source tools (in the middle of a big marketing page advertising monorepos as an ideal): https://monorepo.tools/#monorepo-tools
That list includes several I was aware and several I'd not yet heard of. It's the cross-over between monorepo management tool and build tool is. It's also interesting how many of the open source stacks are purely for or at least heavily specialized for Typescript monorepos.
I don't have any recommendations on which tools work well, just vaguely trying to keep up on the big names in case I need to learn one for a job, or choose one to better organize an existing repo.