Skip to content

Commit a75dece

Browse files
Chore/auto versioning workflow (fmilthaler#111)
This adds another GitHub workflow `AutoVersion` for automated versioning. `AutoVersion` is triggered after PR is merged into `master` or `develop`. It then determines whether the version is a patch or minor change based on the source branch name. - `patch`: version number increase of `0.0.1` - `minor`: version number increase of `0.1.0` - `major`: version number increase of `1.0.0` The branch name patterns that are currently matched to one of the above are: - `patch`: one of `chore/`, `refactor/`, `bugfix/` - `minor`: `feature/` - `major`: None (not planned at the moment) Note: the patterns mentioned above, e.g. `bugfix/` must be the prefix of the branch name. If you are working on a bugfix to fix a print statement of the portfolio properties, your branch name should be something like `bugfix/print-statement-portfolio-properties`. For the automated versioning to work, the branch name is required to **start with** `bugfix/` or one of the other above mentioned patterns. For now, the release number is also updated every time the version number is updated. This practically renders the release number redundant, but it is used in the README.md to show the current release number on PyPI. --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1 parent 9f771bc commit a75dece

File tree

9 files changed

+235
-24
lines changed

9 files changed

+235
-24
lines changed

.github/workflows/auto-format.yml

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
name: AutoUpdateFormatting
1+
name: AutoFormatting
2+
23
on:
34
push:
45
branches:
@@ -22,7 +23,7 @@ jobs:
2223
- name: Set up Python
2324
uses: actions/setup-python@v4
2425
with:
25-
python-version: '3.10' # Specify the desired Python version
26+
python-version: '3.10'
2627

2728
- name: Install dependencies
2829
run: |
@@ -31,15 +32,15 @@ jobs:
3132
- name: Updating README files
3233
id: update_readme
3334
run: |
34-
bash scripts/update-readme.sh
35-
bash scripts/auto-commit.sh "Updating README files"
35+
bash scripts/update_readme.sh
36+
bash scripts/auto_commit.sh "Updating README files"
3637
continue-on-error: true
3738

3839
- name: Code formatting and committing changes
3940
id: code_format
4041
run: |
41-
bash scripts/auto-format.sh
42-
bash scripts/auto-commit.sh "Automated formatting changes"
42+
bash scripts/auto_format.sh
43+
bash scripts/auto_commit.sh "Automated formatting changes"
4344
continue-on-error: true
4445

4546
- name: Push changes
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
name: AutoVersion
2+
on:
3+
pull_request:
4+
types:
5+
- closed
6+
7+
env:
8+
SOURCE_BRANCH: ${{ github.head_ref }}
9+
BASE_BRANCH: ${{ github.base_ref }}
10+
11+
jobs:
12+
auto-versioning:
13+
runs-on: ubuntu-latest
14+
if: ${{ github.event.pull_request.merged == true && (github.event.pull_request.base.ref == 'master' || github.event.pull_request.base.ref == 'develop') }}
15+
steps:
16+
- name: Check out code
17+
uses: actions/checkout@v3
18+
with:
19+
ref: ${{ github.base_ref }}
20+
fetch-depth: 0
21+
22+
- name: Set up Python
23+
uses: actions/setup-python@v4
24+
with:
25+
python-version: '3.10'
26+
27+
- name: Install dependencies
28+
run: |
29+
pip install -r requirements_ci.txt # Adjust as per your project's requirements
30+
31+
- name: Automated versioning and committing changes
32+
id: auto_version
33+
run: |
34+
python scripts/update_version.py ${{ env.BASE_BRANCH }} ${{ env.SOURCE_BRANCH }}
35+
bash scripts/auto_commit.sh "Automated version changes"
36+
continue-on-error: true
37+
38+
- name: Updating README files
39+
id: update_readme
40+
run: |
41+
bash scripts/update_readme.sh
42+
bash scripts/auto_commit.sh "Updating README files"
43+
continue-on-error: true
44+
45+
- name: Push changes to base branch (master or develop)
46+
if: ${{ steps.update_readme.outcome == 'success' || steps.auto_version.outcome == 'success' }}
47+
uses: ad-m/github-push-action@master
48+
with:
49+
branch: ${{ env.BASE_BRANCH }}

.github/workflows/pypi-publish.yml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,24 @@ jobs:
1515
environment:
1616
name: pypi
1717
url: https://pypi.org/p/finquant
18+
1819
steps:
19-
- uses: actions/checkout@v3
20+
- name: Check out code
21+
uses: actions/checkout@v3
22+
2023
- name: Set up Python
2124
uses: actions/setup-python@v4
2225
with:
2326
python-version: '3.10'
27+
2428
- name: Install dependencies
2529
run: |
2630
python -m pip install --upgrade pip
2731
python -m pip install .[cd]
32+
2833
- name: Build package
2934
run: python setup.py sdist bdist_wheel
35+
3036
- name: Publish package to PyPI
3137
uses: pypa/gh-action-pypi-publish@release/v1
3238
with:

CONTRIBUTING.md

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,24 @@ Before creating a new issue/bug report, check the list of existing [issues](http
1616
So you wish to fix bugs yourself, or contribute by adding a new feature. Awesome! Here are a few guidelines I would like you to respect.
1717

1818
### Create a fork
19-
First off, you should create a fork. Within your fork, create a new branch. Depending on what you want to do, choose one of the following prefixes for your branch:
20-
- bugfix/<name of your fix>: to be used for bug fixes
21-
- feature/<name of new feature>: to be used for adding a new feature
19+
First off, you should create a fork. Within your fork, create a new branch. Depending on what you want to do,
20+
choose one of the following prefixes for your branch name:
21+
- `bugfix/` followed by something like `<name of your fix>`: to be used for bug fixes
22+
- `feature/` followed by something like `<name of new feature>`: to be used for adding a new feature
23+
24+
If you simply want to refactor the code base, or do other types of chores, use one of the following branch name prefixes:
25+
- `refactor/` followed by something like `<what you are refactoring>`
26+
- `chore/` followed by something like `<other type of contribution>`
27+
28+
**NOTE**: It is _mandatory_ to use one of the above prefixes for your branch name. FinQuant uses GitHub workflows
29+
to automatically bump the version number when a PR is merged into `master` (or `develop`).
30+
The new version number depends on the source branch name of the merged PR.
31+
32+
Example:
33+
. If you are working on a bugfix to fix a print statement of the portfolio properties,
34+
your branch name should be something like bugfix/print-statement-portfolio-properties.
35+
For the automated versioning to work, the branch name is required to start with `bugfix/` or one of the other
36+
above mentioned patterns.
2237

2338
### Commit your changes
2439
Make your changes to the code, and write sensible commit messages.
@@ -27,16 +42,22 @@ Make your changes to the code, and write sensible commit messages.
2742
In the root directory of your version of FinQuant, run `make test` and make sure all tests are passing.
2843
If applicable, add new tests in the `./tests/` directory. Tests should be written with `pytest`.
2944

45+
Some few tests require you to have a [Quandl API key](https://docs.quandl.com/docs#section-authentication).
46+
If you do not have one locally, you can ignore the tests that are failing due to a missing Quandl API key.
47+
Once you open a PR, all tests are run by GitHub Actions with a pre-configured key.
48+
3049
### Documentation
31-
If applicable, please add docstrings to new functions/classes/modules. Follow example of existing docstrings. FinQuant uses `sphinx` to generate Documentation for [ReadTheDocs](https://finquant.readthedocs.io) automatically from docstrings.
50+
If applicable, please add docstrings to new functions/classes/modules.
51+
Follow example of existing docstrings. FinQuant uses `sphinx` to generate Documentation
52+
for [ReadTheDocs](https://finquant.readthedocs.io) automatically from docstrings.
3253

3354
### Style
34-
To keep everything consistent, please use [Black](https://github.com/psf/black) with default settings.
55+
Fortunately for you, you can ignore code formatting and fully focus on your contribution.
56+
FinQuant uses a GitHub workflow that is automatically triggered and runs [Black](https://github.com/psf/black) and
57+
[isort](https://pycqa.github.io/isort/) to format the code base for you.
3558

3659
### Create a Pull Request
37-
Create a new [Pull Request](https://github.com/fmilthaler/FinQuant/pulls). Describe what your changes are in the Pull Request. If your contribution fixes a bug, or adds a features listed under [issues](https://github.com/fmilthaler/FinQuant/issues) as "#12", please add "fixes #12" or "closes #12".
38-
39-
If you do not have a [Quandl API key](https://docs.quandl.com/docs#section-authentication) set as a secret in your fork, some of the tests are going to fail.
40-
While you are working on your fork, you can ignore the Quandl specific tests that are failing. Once you are happy with your work,
41-
open a PR and use `master` as the target branch. GitHub Actions is set up to run the tests automatically for PRs, and the
42-
Quandl tests are then run with my secret API key.
60+
Create a new [Pull Request](https://github.com/fmilthaler/FinQuant/pulls).
61+
Describe what your changes are in the Pull Request.
62+
If your contribution fixes a bug, or adds a features listed under
63+
[issues](https://github.com/fmilthaler/FinQuant/issues) as "#12", please add "fixes #12" or "closes #12".
File renamed without changes.
File renamed without changes.

scripts/update-readme.sh renamed to scripts/update_readme.sh

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
#!/bin/bash
22

3-
update_version_readme() {
3+
update_release_readme() {
44
local version_file="version"
55
local readme_md="$1"
66

7-
# Read the current version from the "version" file
8-
local current_version=$(grep -Eo 'version=([0-9]+\.){2}[0-9]+' "$version_file" | cut -d'=' -f2)
7+
# Read the current release from the "version" file
8+
local current_release=$(grep -Eo 'release=([0-9]+\.){2}[0-9]+' "$version_file" | cut -d'=' -f2)
99

1010
update_file() {
1111
local file=$1
12-
sed -i "s/pypi-v[0-9]\+\.[0-9]\+\.[0-9]\+/pypi-v$current_version/" "$file"
13-
echo "Version updated to $current_version in $file"
12+
sed -i "s/pypi-v[0-9]\+\.[0-9]\+\.[0-9]\+/pypi-v$current_release/" "$file"
13+
echo "Release updated to $current_release in $file"
1414
}
1515

1616
# Update version in README.md
@@ -39,5 +39,5 @@ update_readme_tex() {
3939

4040
# Update both readme files:
4141
echo "Updating README files:"
42-
update_version_readme "README.md"
42+
update_release_readme "README.md"
4343
update_readme_tex "README.tex.md"

scripts/update_version.py

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import argparse
2+
import re
3+
4+
# Define the version increments based on the change type (patch, minor, major)
5+
version_increments = {
6+
"patch": "0.0.1",
7+
"minor": "0.1.0",
8+
"major": "1.0.0",
9+
}
10+
11+
# Define the branch name prefixes for each change type
12+
branch_prefixes = {
13+
"patch": ["chore/", "refactor/", "bugfix/"],
14+
"minor": ["feature/"],
15+
"major": None,
16+
}
17+
18+
19+
class VersionFileReadError(Exception):
20+
pass
21+
22+
23+
# Function to increment the version based on the branch name pattern
24+
def increment_version(version, branch_name):
25+
for change_type, prefixes in branch_prefixes.items():
26+
prefixes = prefixes or [] # If None, set to an empty list
27+
for prefix in prefixes:
28+
if branch_name.startswith(prefix):
29+
increment = version_increments[change_type]
30+
return (
31+
increment_version_by(version, increment) if increment else version
32+
)
33+
34+
return version
35+
36+
37+
# Function to increment the version by a given increment (e.g., "0.0.1" or "0.1.0" or "1.0.0")
38+
def increment_version_by(version, increment):
39+
version_parts = version.split(".")
40+
increment_parts = increment.split(".")
41+
42+
new_version_parts = []
43+
for i in range(len(version_parts)):
44+
if i < len(increment_parts):
45+
new_version_parts.append(
46+
str(int(version_parts[i]) + int(increment_parts[i]))
47+
)
48+
else:
49+
new_version_parts.append("0")
50+
51+
# If increment is "0.1.0", reset the third digit to 0
52+
if increment == "0.1.0" and len(version_parts) > 2:
53+
new_version_parts[2] = "0"
54+
55+
# If increment is "1.0.0", reset the second and third digit to 0
56+
if increment == "1.0.0" and len(version_parts) > 2:
57+
new_version_parts[1] = "0"
58+
new_version_parts[2] = "0"
59+
60+
return ".".join(new_version_parts)
61+
62+
63+
# Read the version from the file
64+
def read_version_from_file(filename):
65+
with open(filename, "r") as file:
66+
version_content = file.read()
67+
version_match = re.search(r"version=(\d+\.\d+\.\d+)", version_content)
68+
69+
if version_match:
70+
version = version_match.group(1)
71+
else:
72+
version = None
73+
74+
return version
75+
76+
77+
# Write the updated version back to the file
78+
def write_version_to_file(filename, version):
79+
with open(filename, "r+") as file:
80+
file_content = file.read()
81+
updated_content = re.sub(
82+
r"version=\d+\.\d+\.\d+", f"version={version}", file_content
83+
)
84+
# Always set the release number to match the updated version number
85+
updated_content = re.sub(
86+
r"release=\d+\.\d+\.\d+", f"release={version}", updated_content
87+
)
88+
file.seek(0)
89+
file.write(updated_content)
90+
file.truncate()
91+
92+
93+
# Function to parse command-line arguments
94+
def parse_args():
95+
parser = argparse.ArgumentParser(description="Update version based on branch name.")
96+
parser.add_argument("base_branch", help="Base branch name")
97+
parser.add_argument("source_branch", help="Source branch name")
98+
return parser.parse_args()
99+
100+
101+
# Main function
102+
def main():
103+
args = parse_args()
104+
base_branch_name = args.base_branch
105+
source_branch_name = args.source_branch
106+
107+
file_path = "version"
108+
109+
if base_branch_name not in ["main", "develop"]:
110+
raise ValueError("Base branch name must be 'main' or 'develop'.")
111+
112+
if source_branch_name is None:
113+
raise ValueError("Source branch name must not be empty/None.")
114+
115+
current_version = read_version_from_file(file_path)
116+
if current_version is None:
117+
raise VersionFileReadError("Failed to read the current version from the file.")
118+
119+
updated_version = increment_version(current_version, source_branch_name)
120+
print(f"Base branch: {base_branch_name}")
121+
print(f"Source branch: {source_branch_name}")
122+
print(f"Current version: {current_version}")
123+
print(f"Updated version: {updated_version}")
124+
125+
if updated_version == current_version:
126+
print("Version did not change.")
127+
else:
128+
write_version_to_file(file_path, updated_version)
129+
print("Version updated in the file.")
130+
131+
132+
if __name__ == "__main__":
133+
main()

tests/test_portfolio.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
# testing modules portfolio, optimisation, efficient_frontier #
33
# all through the interfaces in Portfolio #
44
###############################################################
5+
56
import datetime
67
import os
78
import pathlib

0 commit comments

Comments
 (0)