Hotfix provenance
A hotfix branch is cut from the release running in production, and the fix is carried over from the default branch. Hotfix provenance makes sure that is all that happens on it. On a branch that matches your hotfix pattern (hotfix/* by default), every commit must be:
- a
git cherry-pick -xof a commit on the repository’s default branch; - whose merged pull request carries your fast-track label (
fast-trackby default); - and whose change is the same as that source commit’s.
fremforge checks every commit of a pull request into a hotfix branch, and every commit pushed to one, and posts the result as the commit status fremforge/hotfix-provenance: success, or failure naming the first commit that does not pass and why. You can make it a required check.
Hotfix provenance is off by default. An org owner switches it on through the API.
Making a hotfix
# 1. Fix it on the default branch first, through a pull request with the label `fast-track`.
# 2. Cut the hotfix branch from the production tag and push it:
git switch -c hotfix/1.4.2 v1.4.1
git push -u origin hotfix/1.4.2
# 3. Carry the fix over on a branch of its own. -x writes the "(cherry picked from commit ...)" line:
git switch -c backport/login-fix
git cherry-pick -x <the commit the merge put on main>
git push -u origin backport/login-fix
# 4. Open a pull request from backport/login-fix into hotfix/1.4.2.Cherry-pick the commit the merge put on the default branch: the squash commit, or the merge commit (git cherry-pick -x -m 1 <merge>). fremforge finds the source’s pull request from that commit. With rebase merges, only the last commit of a rebased pull request can be found this way.
What fails, and why
The status names the first commit that fails and the number of others. Every commit is listed in the explanation.
| Problem | What it means |
|---|---|
| no “cherry picked from commit” line | The commit was not made with git cherry-pick -x. A shortened commit id does not count. |
| a merge commit | Merge commits are not accepted on a hotfix branch, including a merge of the production tag. Cut the branch from the tag and cherry-pick onto it. |
source … is not on main | The commit it was picked from is not on the default branch, for example it is still on a feature branch. |
| source … is not a commit of this repository | The id in the line does not exist in this repository. |
| source … was not merged by a pull request | The source reached the default branch without a pull request. |
| source pull request #… lacks the label | Add the label to that pull request. It counts even after the merge (see below). |
| its change differs from source … | The cherry-pick changed something the source did not, for example a conflict resolved by hand. |
| names several source commits | With the change comparison on, cherry-pick one commit per commit. |
| more than 50 commits | A hotfix carries at most 50 commits. |
How fremforge decides:
- Which commits count. For a pull request into a hotfix branch, its own commits. For a push to a hotfix branch, every commit on the branch that is not on the default branch. The production tag the branch was cut from is on the default branch, so it never counts. Cut hotfix branches from a commit of the default branch.
- Force-pushes are checked from scratch: fremforge looks at the whole branch, never only the commits of the last push.
- The change comparison works like
git patch-id: it compares the lines each commit adds and removes, file by file, ignoring whitespace, line numbers and the surrounding lines. A cherry-pick onto an older release therefore matches its source. You can switch it off (diff_check: false). - A label added after the merge counts. fremforge reads the source pull request’s labels when it checks. Adding the label to a merged pull request re-checks the open hotfix pull requests of that repository within seconds.
- Commits by bots get no exemption. A bot’s backport is a cherry-pick like anyone’s.
- After a pull request merges. On a push, the squash or merge commit that a merged pull request into the same hotfix branch created counts as checked, provided
fremforge/hotfix-provenancepassed on that pull request.
The status never passes by mistake
If fremforge cannot finish the check, for example because the repository could not be read in time (30 seconds), the status is pending, saying so, and it is checked again within 5 minutes. It is never success for a commit it could not check.
On a branch that is not a hotfix branch, fremforge posts nothing, unless that branch’s rule requires fremforge/hotfix-provenance. Then it posts success, “Not a hotfix branch”, so no pull request waits for it. The same applies to a repository where hotfix provenance is off: “not evaluated”.
fremforge posts the status within seconds of a pull request being opened, pushed to, edited or retargeted, and of a push to a hotfix branch. A job every 5 minutes checks any open hotfix pull request whose status is missing, pending or failure again.
Switch it on
curl -X PUT https://frem.sh/_app/api/v1/orgs/<org>/hotfix-provenance/settings \
-H "Authorization: Bearer $FREMFORGE_TOKEN" \
-H "Content-Type: application/json" \
-d '{"mode": "on", "branch_pattern": "hotfix/*", "required_label": "fast-track"}'| Field | Notes |
|---|---|
mode | off or on. Required. |
branch_pattern | The branches that are hotfix branches. * matches within one level, ** across levels: hotfix/*, release/**. A pattern that matches every branch is refused. Unchanged if left out. |
required_label | The label the source pull request must carry. Compared without regard to case. Unchanged if left out. |
diff_check | true (the default) to require each cherry-pick’s change to equal its source’s. Unchanged if left out. |
repos | Per-repository settings, replacing the current list: [{"repo": "api", "enabled": true, "branch_pattern": "release/**", "required_label": "backport"}]. enabled switches one repository on while the organisation is off, to try it out, or off while it is on. Unchanged if left out. |
The token needs policy:write; GET on the same path needs policy:read. Every change is audited as hotfix_provenance.settings_updated.
Make it a required check
curl -X PUT https://frem.sh/_app/api/v1/orgs/<org>/hotfix-provenance/enforcement \
-H "Authorization: Bearer $FREMFORGE_TOKEN" \
-H "Content-Type: application/json" \
-d '{"repo": "*"}'repo is a repository name, or * for every repository, including ones created later. fremforge first posts the status on every open pull request into a hotfix branch, and only then adds fremforge/hotfix-provenance to the branch rule named like the pattern (for example a rule named hotfix/*), keeping the rule’s other checks. A merge from the web page, the API or the merge queue then waits for it.
fremforge never creates a branch rule. A repository without a rule for the pattern is listed in unprotected; create the rule (Settings → Branches), and the job adds the check within 5 minutes. DELETE on the same path with the same body removes it again. The token needs policy:write. Both are audited, as hotfix_provenance.enforcement_enabled and hotfix_provenance.enforcement_disabled.
Why a pull request passes or fails
curl https://frem.sh/_app/api/v1/orgs/<org>/repos/<repo>/pulls/<number>/hotfix-provenance \
-H "Authorization: Bearer $FREMFORGE_TOKEN"The answer lists every commit with whether it passed, the source commit its line names, the source’s pull request, and for a commit that failed the problem and the reason in words. It works while hotfix provenance is off too, so you can try it on a real pull request first. The token needs findings:read, or read access to the repository.