The release process, including last minute changes to fix bugs and
CVEs, happens in a private repo "commercial". The normal OSS
repository is "origin". The "commercial/main-internal" branch has the
same source code as "origin/main", but different CI set up. Therefore
github actions, in the .github directory, are definitely different,
and more than likely there are additional Maven settings in
.mvn. There might be differences between the pom.xml contents in
"main-internal" vs "main", but ideally we want those to go away to
make merging easier later.
-
Create the new "main-internal" branch from "main":
$ git checkout main $ git checkout -b main-internal -
Tweak the CI (action) and build config so that snapshots are deployed to a private artifactory (the CI action will have a secret for the credentials).
-
Run the workflow generator and maybe copy the "release-train-build" and "release-train-test" actions from a similar project.
-
Commit and push to the private repo:
$ git add -A . $ git commit -m "Add build actions for release process" $ git add remote commercial github.com:spring-projects/something-commercial $ git push commercial --set-upstream -
Go into the GitHub UI and make "main-internal" the default branch for this repo.
Commercial releases happen from the private repository on a branch that sits on top of an older branch in the public repository. All the changes to the code in this branch only ever get pushed to the private repository. That makes it easy to look after.
-
Switch the remote tracking branch on the existing branch, e.g. "1.0.x":
$ git checkout 1.0.x $ git push commercial 1.0.x --set-upstream -
Copy the CI and build config from a known good location in the private repo, e.g:
$ rm -rf .mvn .github $ git checkout main-internal -- .mvn .github $ // ... tweak if necessary $ git add -A . $ git commit -m "Migrate build configuration to commercial" $ git push
-
Sync the working copy with the remotes
$ git checkout main-internal $ git pull --rebase $ git checkout main $ git pull --rebase -
Merge "main" into "main-internal" to pick up public changes in the private repo. The (snapshot) version should be the same to reduce risk of conflicts.
$ git checkout main-internal $ git merge main --no-ffShould be clean (no changes to actions, no conflicts). If there are issues it's likely the main-internal branch was set up wrong somehow. It might be better to fix it (at the cost of possibly re-writing history in the private repo) than carry on.
-
Make a release branch and publish it so it can join the release train.
$ git checkout -b release/1.1.1 $ git push commercial release/1.1.1 --set-upstream -
We then make a new branch e.g. "work" for functional changes based off main. We make our changes there and commit. We do not update versions.
$ git checkout main $ git checkout -b work $ # ... make changes $ git add -A . $ git commit -m "Fix a bug"If we make the changes on "main-internal" or "release/1.1.1" they will leak the contents of the private repository when they are merged back into main. If we make changes on main we might accidentally push it to the public repository before the changes are in the open. To make this work, we just have to be careful not to push anything to the public repository until it's ready.
-
If we need to publish snapshots for other projects to consume internally. We will need the functional changes to be merged onto a branch in the private repository, probably the release branch (but it doesn't matter). For example, to merge functional changes into the release branch:
$ git checkout release/1.1.1 $ git merge work --no-ff(Assuming snapshots are produced from this branch. If they come from "main-internal" just reverse the two labels.)
We can do this on every commit if we want to (and each one will produce a new snapshot if the branch we push to has a CI action.
-
Whenever we feel comfortable we can also merge the changes onto the other private branch, in this case "main-internal"
$ git checkout main-internal $ git merge release/1.1.1 --no-ff -
Update project versions in the release branch, commit and test. This way we make the smallest amount of changes in commits that cannot merge cleanly back to main.
$ git commit -am "Update to version 1.1.1" $ git push release/1.1.1 -
Update dependency versions (to values that might not yet exist but will be created during the release process). Commit and push. Tests will fail until the dependencies are available.
$ git commit -am "Update dependencies" $ git push release/1.1.1Run the "ready" workflow in github. Experience release nirvana.
-
Merge "work" back to "main". If there were no changes on "main" then this is equivalent to a hard reset of "main" to the HEAD of "work". There might be additional changes from pull requests or whatever.
$ git checkout main $ git merge --no-ff work $ git branch -d work -
Merge "main" into "main-internal" ready for the next release. If there are no changes it doesn't matter this is a useful synchronization point.
$ git checkout main-internal $ git merge --no-ff mainShould be clean because we only made changes to functional source code, not actions or build configurations.
-
Once we push all the local branches to their respective remotes, the release is finished and project is ready to move on. Note that up to now the only branch we needed to push was the release branch, and that was in a private repo, so if we mess anything up we can go back and do it all again and pay no cost in the public repository.
$ git push -f origin main $ git push -f commercial main-internal
If we prefer to use the release branch for making the functional changes in step 4, we can, but it will make the merge back to the public repo awkward.
-
(Alternatively to steps 4, 5 and 6). If we prefer to work on the release branch until the last minute, we can make all of these changes and then merge back into "main-internal" by picking the last non-destructive commit (usually the last one that is purely functional), e.g.
$ git checkout main-internal $ git merge release/1.1.1~2 -
Cherry pick all the changes from step 1 onto "main" (i.e. everything in "main-internal" since the last time it was merged with "main", but nothing from the release branch that isn't also in "main-internal"). E.g. for the last 3 commits on "main-internal":
$ git checkout main $ git cherry-pick main-internal~2 $ git cherry-pick main-internal~1 $ git cherry-pick main-internalIf there are changes in "main-internal" that are not functional, they don't belong on "main". So be careful to only make non-functional changes in separate commits and to not cherry pick those ones.
We can't merge here because that would change all the actions and build configuration on "main". See alternative strategy below.
If we really want to make the local release branch the working copy, and we don't like manually cherry picking changes from "main-internal" onto "main" (who does?), the only other option is to copy all the changes on "main-internal" from the private repository to the public one. This reveals the contents of the private repository, but there's nothing secret there, so maybe that's an option.
-
At some point before we make any functional changes in "main-internal" (or the release branch), merge it with "main" using the "ours" strategy.
$ git checkout main $ git merge -s ours main-internalThis can be nerve-wracking. It basically tells git to ignore all changes on "main-internal" that are not already on "main" (so all the actions and build configurations) but at the same time, necessarily, it makes those commits ancestors of the "main" HEAD, so they will become visible in the public repository.
If we ever make any more changes to the build configuration in the private repo, this might be the last time we have to do this merge. But if we do ever make changes to the actions and build configurations, we have to do another merge like this to tell git to forget about it when we merge real content.
-
Make the functional changes on whichever branch we prefer and then make sure they are on "main-internal" so that the only commits on "main-internal" since the "ours" merge are purely functional. This is step 2 above, and it gets us ready for the merge in the next step.
-
Instead of cherry picking now we can do a single merge from our working branch to main.
$ git checkout main $ git merge --no-ff main-internalThere can be conflicts but they should be minor (formatting changes etc.), and there should be no further cross-contamination of the "private" repo contents, i.e. it stays in visible but ignored ancestor commits.
If our release branch is ready to ship we could probably do this as an alternative:
$ git checkout main $ git merge --no-ff release/1.1.1~2(Assuming the last 2 commits were version changes that we don't want in "main". Or maybe we do want the dependencies updated, but not the project version, in which case probably "release/1.1.1~1 would be better).
This is simpler because nothing has to get merged back into the public repository - in fact there is only one repository. Assume we want to release a new version of "1.0.x"
-
Sync the working copy with the remotes
$ git checkout 1.0.x $ git pull --rebase -
Make a release branch and publish it so it can join the release train.
$ git checkout -b release/1.0.1 $ git push commercial release/1.0.1 --set-upstream -
Work in the release branch and make functional changes as needed. We make our changes there and commit. We do not update versions.
$ git checkout release/1.0.1 $ # ... make changes $ git add -A . $ git commit -m "Fix a bug" -
Update project versions in the release branch, commit and test. This way we make the smallest amount of changes in commits that cannot merge cleanly back to main.
$ git commit -am "Update to version 1.0.1" $ git push release/1.0.1 -
Update dependency versions (to values that might not yet exist but will be created during the release process). Commit and push. Tests will fail until the dependencies are available.
$ git commit -am "Update dependencies" $ git push release/1.0.1Run the "ready" workflow in github. Experience release nirvana.
-
Merge the release branch back to its ancestor.
$ git checkout 1.0.x $ git merge --no-ff release/1.0.1~2 $ git push(Using "~2" to skip the last 2 commits. If we had more non-functional commits, then we could adapt accordingly.)
-
Once we push all the local branches to their respective remotes, the release is finished and project is ready to move on.
$ git push -f origin main $ git push -f commercial main-internal
The process is identical to an OSS release but with a micro version identifier.