diff --git a/.github/workflows/merge_bugfix.yml b/.github/workflows/merge_bugfix.yml new file mode 100644 index 0000000000..06385edb94 --- /dev/null +++ b/.github/workflows/merge_bugfix.yml @@ -0,0 +1,41 @@ +name: Merge bugfix into feature branch +on: + push: + branches: + - 'bugfix' +jobs: + merge_bugfix: + name: Merge bugfix into feature branch + runs-on: ubuntu-latest + if: "!contains(github.event.head_commit.message, '[Bot]')" + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + ref: feature + - name: Reset bugfix branch + run: | + export FEATURE_VERSION="$(cat VERSION)" + git fetch origin bugfix:bugfix + git reset --hard bugfix + echo $FEATURE_VERSION > VERSION + - name: Generate token + uses: actions/create-github-app-token@v1 + id: app-token + with: + app-id: ${{ secrets.TOKEN_APP_ID }} + private-key: ${{ secrets.TOKEN_APP_SECRET }} + - name: Submit pull request + id: pull-request + uses: peter-evans/create-pull-request@v6 + with: + token: ${{ steps.app-token.outputs.token }} + commit-message: "[Bot] Merge bugfix into feature branch." + branch: merge-bugfix + title: "Merge bugfix into feature branch" + labels: bot + body: "Merge branch \"bugfix\" into \"feature\"." + - name: Enable auto-merge + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + run: gh pr merge --merge --auto "${{ steps.pull-request.outputs.pull-request-number }}" diff --git a/.github/workflows/merge_feature.yml b/.github/workflows/merge_feature.yml new file mode 100644 index 0000000000..efbdd52da7 --- /dev/null +++ b/.github/workflows/merge_feature.yml @@ -0,0 +1,41 @@ +name: Merge feature into main branch +on: + push: + branches: + - 'feature' +jobs: + merge_feature: + name: Merge feature into main branch + runs-on: ubuntu-latest + if: "!contains(github.event.head_commit.message, '[Bot]')" + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + ref: main + - name: Reset feature branch + run: | + export MAIN_VERSION="$(cat VERSION)" + git fetch origin feature:feature + git reset --hard feature + echo $MAIN_VERSION > VERSION + - name: Generate token + uses: actions/create-github-app-token@v1 + id: app-token + with: + app-id: ${{ secrets.TOKEN_APP_ID }} + private-key: ${{ secrets.TOKEN_APP_SECRET }} + - name: Submit pull request + id: pull-request + uses: peter-evans/create-pull-request@v6 + with: + token: ${{ steps.app-token.outputs.token }} + commit-message: "[Bot] Merge feature into main branch." + branch: merge-feature + title: "Merge feature into main branch" + labels: bot + body: "Merge branch \"feature\" into \"main\"." + - name: Enable auto-merge + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + run: gh pr merge --merge --auto "${{ steps.pull-request.outputs.pull-request-number }}" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4b32f79a6e..84ccf8cc7d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,19 +3,61 @@ on: release: types: [ published ] jobs: - reset_development_version: - name: Reset development version + merge_release: + name: Merge into upstream branches runs-on: ubuntu-latest - if: ${{ github.event.release.target_commitish == 'main' }} steps: + - name: Generate token + uses: actions/create-github-app-token@v1 + id: app-token + with: + app-id: ${{ secrets.TOKEN_APP_ID }} + private-key: ${{ secrets.TOKEN_APP_SECRET }} - name: Checkout uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ steps.app-token.outputs.token }} - name: Setup git uses: fregante/setup-git-user@v2 - - name: Reset development version + - name: Checkout release branch + run: | + git checkout ${{ github.event.release.target_commitish }} + - name: Update major version + if: ${{ github.event.release.target_commitish == 'main' }} run: | ./build reset_development_version - git add VERSION.dev && git commit -m "[Bot] Update development version to $(cat VERSION.dev)." && git push + git add VERSION.dev && git commit -m "[Bot] Update development version to $(cat VERSION.dev)." && git push origin main + ./build increment_major_version + git add VERSION + git commit -m "[Bot] Update version to $(cat VERSION)." + git push origin main + - name: Merge into feature branch + if: ${{ github.event.release.target_commitish == 'main' }} + run: | + git checkout -b feature origin/feature + git merge origin/main --strategy-option theirs -m "[Bot] Merge branch \"main\" into \"feature\"." + ./build increment_minor_version + git add VERSION + git commit -m "[Bot] Update version to $(cat VERSION)." + git push origin feature + - name: Merge into bugfix branch + if: ${{ github.event.release.target_commitish == 'main' }} || ${{ github.event.release.target_commitish == 'feature' }} + run: | + git checkout -b bugfix origin/bugfix + git merge origin/feature --strategy-option theirs -m "[Bot] Merge branch \"main\" into \"bugfix\"." + ./build increment_patch_version + git add VERSION + git commit -m "[Bot] Update version to $(cat VERSION)." + git push origin bugfix + - name: Update patch version + if: ${{ github.event.release.target_commitish == 'bugfix' }} + run: | + git checkout -b bugfix origin/bugfix + ./build increment_patch_version + git add VERSION + git commit -m "[Bot] Update version to $(cat VERSION)." + git push origin bugfix publish_packages: name: Publish wheel packages uses: ./.github/workflows/template_publish.yml diff --git a/.github/workflows/release_development.yml b/.github/workflows/release_development.yml index 5e64778689..26f00b8af1 100644 --- a/.github/workflows/release_development.yml +++ b/.github/workflows/release_development.yml @@ -12,6 +12,7 @@ on: jobs: update_development_version: name: Update development version + if: "!contains(github.event.head_commit.message, '[Bot]')" runs-on: ubuntu-latest steps: - name: Checkout diff --git a/.github/workflows/test_file_changes.yml b/.github/workflows/test_file_changes.yml index d6610ad73d..ddbac04976 100644 --- a/.github/workflows/test_file_changes.yml +++ b/.github/workflows/test_file_changes.yml @@ -26,7 +26,7 @@ jobs: uses: xalvarez/prevent-file-change-action@v1 with: githubToken: ${{ steps.app-token.outputs.token }} - pattern: ^VERSION.dev$ + pattern: ^VERSION(.dev)?$ allowNewFiles: true - name: Save Git repository to cache uses: actions/cache/save@v4 diff --git a/CHANGELOG.md b/CHANGELOG.md index 34dfedcee5..26f7a42b93 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,10 +22,11 @@ This release comes with several API changes. For an updated overview of the avai - **The value to be used for sparse elements of a feature matrix** can now be specified via the C++ or Python API. - **Nominal and ordinal feature values are now represented as integers** to avoid issues due to limited floating point precision. - **Safe comparisons of floating point values** are now used to avoid issues due to limited floating point precision. +- **Fundamental data structures for vectors and matrices have been reworked** to ease reusing existing functionality and avoiding redundant code. ### Additions to the Command Line API -- **Information about the program can now be printed** via the argument `-v` or `--version`. +- **Information abused to implementout the program can now be printed** via the argument `-v` or `--version`. - **Data characteristics do now include the number of ordinal attributes** when printed on the console or written to a file via the command line argument `--print-data-characteristics` or `--store-data-characteristics`. ### Bugfixes @@ -45,11 +46,10 @@ This release comes with several API changes. For an updated overview of the avai - Added support for unit testing the project's C++ code. Compilation of the tests can be disabled via a build option. - The Python code is now checked for common issues by applying `pylint` via continuous integration. - The Makefile has been replaced with wrapper scripts triggering a [SCons](https://scons.org/) build. -- Development versions of wheel packages are now frequently built via continuous integration, uploaded as artifacts, and published on [Test-PyPI](https://test.pypi.org/). +- Development versions of wheel packages are now regularly built via continuous integration, uploaded as artifacts, and published on [Test-PyPI](https://test.pypi.org/). +- Continuous integration is now used to maintain separate branches for major, feature, and bugfix releases and keep them up-to-date. - The runtime of continuous integration jobs has been optimized by running individual steps only if necessary, caching files across subsequent runs, and making use of parallelization. -- When built via continuous integration, libraries and the documentation are now uploaded as artifacts. - When tests are run via continuous integration, a summary of the test results is now added to merge requests and Github workflows. -- The fundamental data structures used to implement vectors and matrices have been reworked to ease reusing existing functionality and avoiding redundant code. - Markdown files are now used for writing the documentation. - A consistent style is now enforced for Markdown files by applying the tool `mdformat` via continuous integration. - C++ 17 or newer is now required for compiling the project. diff --git a/doc/developer_guide/coding_standards.md b/doc/developer_guide/coding_standards.md index 32c4c2fc7d..452893ac03 100644 --- a/doc/developer_guide/coding_standards.md +++ b/doc/developer_guide/coding_standards.md @@ -16,13 +16,14 @@ A track record of past runs can be found on Github in the [Actions](https://gith The workflow definitions of individual CI jobs can be found in the directory [.github/workflows/](https://github.com/mrapp-ke/MLRL-Boomer/tree/8ed4f36af5e449c5960a4676bc0a6a22de195979/.github/workflows). Currently, the following jobs are used in the project: -- `release.yml` is used for publishing pre-built packages on [PyPI](https://pypi.org/) (see {ref}`installation`). For this purpose, the project is built from source for each of the target platforms and architectures, using virtualization in some cases. The job is run automatically when a new release was published on [Github](https://github.com/mrapp-ke/MLRL-Boomer/releases). +- `release.yml` is used for publishing pre-built packages on [PyPI](https://pypi.org/) (see {ref}`installation`). For this purpose, the project is built from source for each of the target platforms and architectures, using virtualization in some cases. The job is run automatically when a new release was published on [Github](https://github.com/mrapp-ke/MLRL-Boomer/releases). It does also increment the project's major version number and merge the release branch into its upstream branches (see {ref}`release-process`). - `release_development.yml` publishes development versions of packages on [Test-PyPI](https://test.pypi.org/) whenever changes to the project's source code have been pushed to the main branch. The packages built by each of these runs are also saved as [artifacts](https://docs.github.com/en/actions/using-workflows/storing-workflow-data-as-artifacts) and can be downloaded as zip archives. - `test_release.yml` ensures that the packages to be released for different architectures and Python versions can be built. The job is run for pull requests that modify relevant parts of the source code. - `test_build.yml` builds the project for each of the supported target platforms, i.e., Linux, Windows, and MacOS (see {ref}`compilation`). In the Linux environment, this job does also execute all available unit and integration tests (see {ref}`testing`). It is run for pull requests whenever relevant parts of the project's source code have been modified. - `test_doc.yml` generates the latest documentation (see {ref}`documentation`) whenever relevant parts of the source code are affected by a pull request. - `test_format.yml` ensures that all source files in the project adhere to our coding style guidelines (see {ref}`code-style`). This job is run automatically for pull requests whenever they include any changes affecting the relevant source files. - `test_file_changes.yml` prevents pull requests from modifying certain files that must not be modified manually, but are intended to only be updated via Github Actions. +- `merge_feature.yml` and `merge_bugfix.yml` are used to merge changes that have been pushed to the feature or bugfix branch into downstream branches via pull requests (see {ref}`release-process`). (testing)= @@ -142,6 +143,20 @@ Feature releases with the major version `0` are not obliged to maintain API comp Increments of the major version indicate big leaps in the software's development. They are reserved for new versions of the software that introduce new functionality, fundamentally change how the software works, or come with compatibility-breaking changes. In general, major releases are not guaranteed to be compatible with past releases in any way. In particular, they may introduce compatibility-breaking API changes, affecting the command line API or programmatic APIs in the project's Python or C++ code. Moreover, models that have been trained using an older version are not guaranteed to work after updating to a new major release and must potentially be trained from scratch. +(release-process)= + +## Release Process + +To enable releasing new major, feature, or bugfix releases at any time, we maintain a branch for each type of release: + +- `main` contains all changes that will be included in the next major release (including changes on the feature and bugfix branch). +- `feature` comes with the changes that will be part of an upcoming feature release (including changes on the bugfix branch). +- `bugfix` is restricted to minor changes that will be published as a bugfix release. + +We do not allow directly pushing to the above branches. Instead, all changes must be submitted via pull requests and require certain checks to pass. Once modifications to one of the branches have been merged, {ref}`ci` jobs are used to automatically update downstream branches via pull requests. If all checks run for such pull requests are successful, they are merged automatically. If there are any merge conflicts, they must be resolved manually. Following this procedure, changes to the feature brach are merged into the main branch (see `merge_feature.yml`). Changes to the bugfix branch are first merged into the feature branch and then into the main branch (see `merge_bugfix.yml`). + +Whenever a new release has been published, the release branch is merged into the upstream branches (see `release.yml`), i.e., major releases result in the feature and bugfix branches being updated, whereas minor releases result in the bugfix branch to be updated. The version of the release branch and the affected branches are updated accordingly. The version of a branch is specified in the file `VERSION` in the project's root directory. Similarly, the file `VERSION.dev` is used to keep track of the version number used for development releases (see `release_development.yml`). + (dependencies)= ## Dependencies diff --git a/scons/sconstruct.py b/scons/sconstruct.py index 0e4d17a570..8208eb9371 100644 --- a/scons/sconstruct.py +++ b/scons/sconstruct.py @@ -16,7 +16,8 @@ from modules import BUILD_MODULE, CPP_MODULE, DOC_MODULE, PYTHON_MODULE from packaging import build_python_wheel, install_python_wheels from testing import tests_cpp, tests_python -from versioning import increment_development_version, reset_development_version +from versioning import increment_development_version, increment_major_version, increment_minor_version, \ + increment_patch_version, reset_development_version from SCons.Script import COMMAND_LINE_TARGETS from SCons.Script.SConscript import SConsEnvironment @@ -34,6 +35,9 @@ def __print_if_clean(environment, message: str): # Define target names... TARGET_NAME_INCREMENT_DEVELOPMENT_VERSION = 'increment_development_version' TARGET_NAME_RESET_DEVELOPMENT_VERSION = 'reset_development_version' +TARGET_NAME_INCREMENT_PATCH_VERSION = 'increment_patch_version' +TARGET_NAME_INCREMENT_MINOR_VERSION = 'increment_minor_version' +TARGET_NAME_INCREMENT_MAJOR_VERSION = 'increment_major_version' TARGET_NAME_TEST_FORMAT = 'test_format' TARGET_NAME_TEST_FORMAT_PYTHON = TARGET_NAME_TEST_FORMAT + '_python' TARGET_NAME_TEST_FORMAT_CPP = TARGET_NAME_TEST_FORMAT + '_cpp' @@ -61,13 +65,14 @@ def __print_if_clean(environment, message: str): TARGET_NAME_DOC = 'doc' VALID_TARGETS = { - TARGET_NAME_INCREMENT_DEVELOPMENT_VERSION, TARGET_NAME_RESET_DEVELOPMENT_VERSION, TARGET_NAME_TEST_FORMAT, - TARGET_NAME_TEST_FORMAT_PYTHON, TARGET_NAME_TEST_FORMAT_CPP, TARGET_NAME_TEST_FORMAT_MD, TARGET_NAME_FORMAT, - TARGET_NAME_FORMAT_PYTHON, TARGET_NAME_FORMAT_CPP, TARGET_NAME_FORMAT_MD, TARGET_NAME_CHECK_DEPENDENCIES, - TARGET_NAME_VENV, TARGET_NAME_COMPILE, TARGET_NAME_COMPILE_CPP, TARGET_NAME_COMPILE_CYTHON, TARGET_NAME_INSTALL, - TARGET_NAME_INSTALL_CPP, TARGET_NAME_INSTALL_CYTHON, TARGET_NAME_BUILD_WHEELS, TARGET_NAME_INSTALL_WHEELS, - TARGET_NAME_TESTS, TARGET_NAME_TESTS_CPP, TARGET_NAME_TESTS_PYTHON, TARGET_NAME_APIDOC, TARGET_NAME_APIDOC_CPP, - TARGET_NAME_APIDOC_PYTHON, TARGET_NAME_DOC + TARGET_NAME_INCREMENT_DEVELOPMENT_VERSION, TARGET_NAME_RESET_DEVELOPMENT_VERSION, + TARGET_NAME_INCREMENT_PATCH_VERSION, TARGET_NAME_INCREMENT_MINOR_VERSION, TARGET_NAME_INCREMENT_MAJOR_VERSION, + TARGET_NAME_TEST_FORMAT, TARGET_NAME_TEST_FORMAT_PYTHON, TARGET_NAME_TEST_FORMAT_CPP, TARGET_NAME_TEST_FORMAT_MD, + TARGET_NAME_FORMAT, TARGET_NAME_FORMAT_PYTHON, TARGET_NAME_FORMAT_CPP, TARGET_NAME_FORMAT_MD, + TARGET_NAME_CHECK_DEPENDENCIES, TARGET_NAME_VENV, TARGET_NAME_COMPILE, TARGET_NAME_COMPILE_CPP, + TARGET_NAME_COMPILE_CYTHON, TARGET_NAME_INSTALL, TARGET_NAME_INSTALL_CPP, TARGET_NAME_INSTALL_CYTHON, + TARGET_NAME_BUILD_WHEELS, TARGET_NAME_INSTALL_WHEELS, TARGET_NAME_TESTS, TARGET_NAME_TESTS_CPP, + TARGET_NAME_TESTS_PYTHON, TARGET_NAME_APIDOC, TARGET_NAME_APIDOC_CPP, TARGET_NAME_APIDOC_PYTHON, TARGET_NAME_DOC } DEFAULT_TARGET = TARGET_NAME_INSTALL_WHEELS @@ -85,12 +90,11 @@ def __print_if_clean(environment, message: str): env.SConsignFile(name=path.relpath(path.join(BUILD_MODULE.build_dir, '.sconsign'), BUILD_MODULE.root_dir)) # Defines targets for updating the project's version... -target_increment_development_version = __create_phony_target(env, - TARGET_NAME_INCREMENT_DEVELOPMENT_VERSION, - action=increment_development_version) -target_reset_development_version = __create_phony_target(env, - TARGET_NAME_RESET_DEVELOPMENT_VERSION, - action=reset_development_version) +__create_phony_target(env, TARGET_NAME_INCREMENT_DEVELOPMENT_VERSION, action=increment_development_version) +__create_phony_target(env, TARGET_NAME_RESET_DEVELOPMENT_VERSION, action=reset_development_version) +__create_phony_target(env, TARGET_NAME_INCREMENT_PATCH_VERSION, action=increment_patch_version) +__create_phony_target(env, TARGET_NAME_INCREMENT_MINOR_VERSION, action=increment_minor_version) +__create_phony_target(env, TARGET_NAME_INCREMENT_MAJOR_VERSION, action=increment_major_version) # Define targets for checking code style definitions... target_test_format_python = __create_phony_target(env, TARGET_NAME_TEST_FORMAT_PYTHON, action=check_python_code_style) diff --git a/scons/versioning.py b/scons/versioning.py index b80e7e63b9..370a9c93c0 100644 --- a/scons/versioning.py +++ b/scons/versioning.py @@ -5,6 +5,8 @@ """ import sys +from typing import Tuple + VERSION_FILE = 'VERSION' DEV_VERSION_FILE = VERSION_FILE + '.dev' @@ -53,6 +55,35 @@ def __update_development_version(dev: int): __write_version_file(DEV_VERSION_FILE, updated_version) +def __parse_version(version: str) -> Tuple[int, int, int]: + parts = version.split('.') + + if len(parts) != 3: + print('Version must be given in format MAJOR.MINOR.PATCH or MAJOR.MINOR.PATCH.devN, but got: ' + version) + sys.exit(-1) + + major = __parse_version_number(parts[0]) + minor = __parse_version_number(parts[1]) + patch = __parse_version_number(parts[2]) + return major, minor, patch + + +def __format_version(major: int, minor: int, patch: int) -> str: + return str(major) + '.' + str(minor) + '.' + str(patch) + + +def __get_current_version() -> Tuple[int, int, int]: + current_version = __read_version_file(VERSION_FILE) + print('Current version is "' + current_version + '"') + return __parse_version(current_version) + + +def __update_version(major: int, minor: int, patch: int): + updated_version = __format_version(major, minor, patch) + print('Updated version to "' + updated_version + '"') + __write_version_file(VERSION_FILE, updated_version) + + def increment_development_version(**_): """ Increments the development version. @@ -66,6 +97,35 @@ def reset_development_version(**_): """ Resets the development version. """ - dev = __get_current_development_version() - dev = 0 - __update_development_version(dev) + __get_current_development_version() + __update_development_version(0) + + +def increment_patch_version(**_): + """ + Increments the patch version. + """ + major, minor, patch = __get_current_version() + patch += 1 + __update_version(major, minor, patch) + + +def increment_minor_version(**_): + """ + Increments the minor version. + """ + major, minor, patch = __get_current_version() + minor += 1 + patch = 0 + __update_version(major, minor, patch) + + +def increment_major_version(**_): + """ + Increments the major version. + """ + major, minor, patch = __get_current_version() + major += 1 + minor = 0 + patch = 0 + __update_version(major, minor, patch)