Aakvatech Limited - How to Add Version-Based Release Tagging and Promotion to a Frappe App Repo
Set up GitHub Actions for a Frappe app repo: create hotfix branches, read __version__, tag releases, and promote reviewed PRs to version branches using labels. Includes a clean release checklist too!!
How to Add Version-Based Release Tagging and Promotion to a Frappe App Repo Using GitHub Actions
Many Frappe app repositories start simple: one or more development branches, no release automation, no hotfix branch, and no GitHub Actions. That works for early development, but it becomes risky when you want repeatable releases.
This guide explains how to set up a clean release flow for a Frappe app where:
- Developers work on a feature or release branch.
- A PR is reviewed into a hotfix/staging branch.
- A GitHub Actions workflow reads the app version from
__init__.py. - The workflow creates a Git tag such as
v15.2.0. - The workflow promotes the merged commit to the correct release branch, such as
version-15.
GitHub Actions workflows are YAML files stored in .github/workflows inside the repository. (GitHub Docs)
Target Release Flow
The final flow will look like this:
release/15.2.0
↓ PR
version-15-hotfix
↓ merge after review
GitHub Actions
↓
create tag v15.2.0
↓
promote commit to version-15
For Frappe v16, the same idea can be used:
release/16.0.1
↓ PR
version-16-hotfix
↓ merge after review
GitHub Actions
↓
create tag v16.0.1
↓
promote commit to version-16
1. Add the App Version in __init__.py
In a Frappe app, keep the app version in the package __init__.py.
Example:
propms/__init__.py
Add or update:
__version__ = "15.2.0"
This is the value the workflow will read.
The workflow will convert:
__version__ = "15.2.0"
into this Git tag:
v15.2.0
Recommended convention:
App version: 15.2.0
Git tag: v15.2.0
Branch: release/15.2.0
Avoid naming the branch v15.2.0, because that makes the branch name and tag name identical.
2. Create Hotfix and Release Branches
If your repository does not already have hotfix branches, create them once.
For Frappe v15:
git checkout main
git pull origin main
git checkout -b version-15-hotfix
git push origin version-15-hotfix
git checkout main
git checkout -b version-15
git push origin version-15
For Frappe v16:
git checkout main
git pull origin main
git checkout -b version-16-hotfix
git push origin version-16-hotfix
git checkout main
git checkout -b version-16
git push origin version-16
Branch purpose:
| Branch | Purpose |
|---|---|
release/15.2.0 |
Development/release preparation branch |
version-15-hotfix |
Review and staging branch for v15 releases |
version-15 |
Final release branch used by deployments |
version-16-hotfix |
Review and staging branch for v16 releases |
version-16 |
Final release branch for v16 deployments |
3. Create Promotion Labels in GitHub
GitHub labels can be applied to pull requests, issues, and discussions, and they are created at the repository level. (GitHub Docs)
Create these labels:
promote/version-15
promote/version-16
In GitHub:
Repository → Issues → Labels → New label
Create:
promote/version-15
and, if needed:
promote/version-16
These labels tell the workflow where to promote the merged commit.
4. Create the GitHub Actions Workflow File
Create this file:
.github/workflows/tag-and-promote-from-pr-label.yml
From the GitHub website, you can create the folders by typing the full file path when creating a new file:
.github/workflows/tag-and-promote-from-pr-label.yml
GitHub will automatically create .github and workflows.
5. Add the Workflow YAML
Paste this into:
.github/workflows/tag-and-promote-from-pr-label.yml
name: Tag and promote from PR label
on:
pull_request:
types:
- closed
permissions:
contents: write
pull-requests: read
jobs:
tag-and-promote:
if: github.event.pull_request.merged == true
runs-on: ubuntu-latest
steps:
- name: Determine target branch from PR labels
id: target
uses: actions/github-script@v7
with:
script: |
const labels = context.payload.pull_request.labels.map(label => label.name);
const mapping = {
"promote/version-15": "version-15",
"promote/version-16": "version-16"
};
const matchedLabels = labels.filter(label => mapping[label]);
if (matchedLabels.length === 0) {
core.info(
`No promote target label found. Skipping promotion. Add one of: ${Object.keys(mapping).join(", ")}`
);
core.setOutput("should_promote", "false");
return;
}
if (matchedLabels.length > 1) {
core.setFailed(
`Multiple promote target labels found: ${matchedLabels.join(", ")}. Keep only one.`
);
return;
}
const matchedLabel = matchedLabels[0];
core.setOutput("should_promote", "true");
core.setOutput("target_branch", mapping[matchedLabel]);
core.setOutput("matched_label", matchedLabel);
- name: Checkout merged commit
if: steps.target.outputs.should_promote == 'true'
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.merge_commit_sha }}
fetch-depth: 0
- name: Read version from propms.__version__
if: steps.target.outputs.should_promote == 'true'
id: version
shell: bash
run: |
VERSION=$(python - <<'PY'
import re
from pathlib import Path
init_file = Path("propms/__init__.py")
content = init_file.read_text()
match = re.search(r'^__version__\s*=\s*[\"\']([^\"\']+)[\"\']', content, re.M)
if not match:
raise SystemExit("Could not find __version__ in propms/__init__.py")
print(match.group(1))
PY
)
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "tag=v$VERSION" >> "$GITHUB_OUTPUT"
- name: Configure git user
if: steps.target.outputs.should_promote == 'true'
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
- name: Create and push version tag
if: steps.target.outputs.should_promote == 'true'
run: |
git fetch --tags
if git rev-parse "${{ steps.version.outputs.tag }}" >/dev/null 2>&1; then
echo "Tag ${{ steps.version.outputs.tag }} already exists."
exit 1
fi
git tag -a "${{ steps.version.outputs.tag }}" -m "Release ${{ steps.version.outputs.tag }}"
git push origin "${{ steps.version.outputs.tag }}"
- name: Promote merged commit to target branch
if: steps.target.outputs.should_promote == 'true'
run: |
git push origin HEAD:${{ steps.target.outputs.target_branch }}
GitHub documents the pull_request closed event with a merged-condition check as the standard way to run automation after a PR is merged. (GitHub Docs)
The workflow uses GITHUB_TOKEN implicitly to push the tag and branch. For that, the workflow declares:
permissions:
contents: write
GitHub documents GITHUB_TOKEN as the built-in authentication token for workflows, and permissions can be configured in the workflow file. (GitHub Docs)
6. Adjust the App Path if Needed
The workflow reads:
propms/__init__.py
If your Frappe app package is not propms, update this line:
init_file = Path("propms/__init__.py")
Examples:
init_file = Path("my_app/__init__.py")
or:
init_file = Path("property_management_solution/__init__.py")
The correct folder is usually the package folder that contains:
hooks.py
Example structure:
apps/propms/
├── propms/
│ ├── __init__.py
│ ├── hooks.py
│ └── property_management_solution/
├── pyproject.toml
└── README.md
In this case, use:
init_file = Path("propms/__init__.py")
7. Commit the Workflow
Commit the workflow to the repository.
git add .github/workflows/tag-and-promote-from-pr-label.yml
git commit -m "ci: add PR label based release tagging and promotion"
git push origin main
It is best to have the workflow file present on the default branch so GitHub can reliably trigger it for PR events.
8. Create a Release Branch for the Change
For a new release:
git checkout main
git pull origin main
git checkout -b release/15.2.0
Update:
propms/__init__.py
Set:
__version__ = "15.2.0"
Commit:
git add propms/__init__.py
git commit -m "chore: bump version to 15.2.0"
git push origin release/15.2.0
9. Open a PR to the Hotfix Branch
Open a pull request:
base: version-15-hotfix
compare: release/15.2.0
Add the label:
promote/version-15
Review the PR as usual.
10. Merge the PR
When the PR is merged into:
version-15-hotfix
the workflow runs automatically.
If the PR has:
promote/version-15
and the app has:
__version__ = "15.2.0"
then the workflow will:
1. Read propms/__init__.py
2. Extract 15.2.0
3. Create Git tag v15.2.0
4. Push tag v15.2.0
5. Push the merged commit to version-15
Final result:
version-15-hotfix contains reviewed release code
version-15 contains promoted release code
v15.2.0 points to the promoted release commit
11. Repeat for Frappe v16
For a v16 release:
base: version-16-hotfix
compare: release/16.0.1
label: promote/version-16
In propms/__init__.py:
__version__ = "16.0.1"
After merge, the workflow creates:
v16.0.1
and promotes the commit to:
version-16
12. Recommended Branch Protection
Protect the final release branches:
version-15
version-16
At minimum, restrict who can push to them.
If branch protection blocks GitHub Actions from pushing to version-15, then either:
- Allow GitHub Actions to push to the branch.
- Use a GitHub App token with permission to bypass restrictions.
- Change the workflow to create a PR into
version-15instead of pushing directly.
For a small team, direct promotion to version-15 is usually fine if only maintainers can merge into version-15-hotfix.
13. What Happens If the Tag Already Exists?
The workflow intentionally fails if the tag already exists:
if git rev-parse "${{ steps.version.outputs.tag }}" >/dev/null 2>&1; then
echo "Tag ${{ steps.version.outputs.tag }} already exists."
exit 1
fi
This protects you from accidentally releasing two different commits with the same version.
If a release needs a fix, bump the patch version:
__version__ = "15.2.1"
Then release:
v15.2.1
Do not reuse:
v15.2.0
14. Suggested Release Checklist
Before opening the PR:
[ ] Version updated in propms/__init__.py
[ ] Branch name is release/x.y.z
[ ] PR is opened against version-15-hotfix or version-16-hotfix
[ ] Correct promote label is added
[ ] Tests/review completed
[ ] PR merged
[ ] GitHub Action succeeded
[ ] Tag created
[ ] Release branch updated
Example for v15:
[ ] propms/__init__.py has __version__ = "15.2.0"
[ ] PR: release/15.2.0 → version-15-hotfix
[ ] Label: promote/version-15
[ ] Tag created: v15.2.0
[ ] Branch updated: version-15
Conclusion
This setup gives a Frappe app repository a clean and auditable release process without requiring manual tagging or direct pushes to release branches.
The key ideas are:
__init__.py controls the app version
PR labels control the release target
GitHub Actions creates the tag
GitHub Actions promotes reviewed code to the release branch
For most Frappe app repositories, this is a practical middle ground between fully manual releases and a complex CI/CD pipeline.
No comments yet. Login to start a new discussion Start a new discussion