Commit ff8b9856 authored by Pierre Smeyers's avatar Pierre Smeyers
Browse files

feat(release): complete release process refactoring

BREAKING CHANGE: complete refactoring or release process, including variables and jobs redefinition
- no more separate publish job: the entire release process is now performed by the py-release job
- TWINE_XXX variables removed and replaced by PYTHON_REPOSITORY_XXX
- RELEASE_VERSION_PART variable replaced by PYTHON_RELEASE_NEXT

For additional info, see the doc.
parent 319f85c4
Loading
Loading
Loading
Loading
+74 −38
Original line number Diff line number Diff line
@@ -50,6 +50,12 @@ and/or `setup.py` and/or `requirements.txt`), but the build system might also be

## Jobs

### `py-package` job

This job allows building your Python project [distribution packages](https://packaging.python.org/en/latest/glossary/#term-Distribution-Package).

It is bound to the `build` stage, it is **disabled by default** and can be enabled by setting `$PYTHON_PACKAGE_ENABLED` to `true`.

### Lint jobs

#### `py-pylint` job
@@ -240,56 +246,86 @@ It is bound to the `test` stage, and uses the following variables:
This job outputs a **textual report** in the console, and in case of failure also exports a JSON report in the `reports/`
directory _(relative to project root dir)_.

### Package jobs
### `py-release` job

This job is **disabled by default** and allows to perform a complete release of your Python code:

#### `py-package` job
1. increase the Python project version,
2. Git commit changes and create a Git tag with the new version number,
3. build the [Python packages](https://packaging.python.org/),
4. publish the built packages to a PyPI compatible repository ([GitLab packages](https://docs.gitlab.com/ee/user/packages/pypi_repository/) by default).

This job is **disabled by default** and performs a packaging of your Python code.
The Python template supports two packaging systems:

It is bound to the `package-build` stage, applies only on git tags and uses the following variables:
* [Poetry](https://python-poetry.org/): uses Poetry-specific [version](https://python-poetry.org/docs/cli/#version), [build](https://python-poetry.org/docs/cli/#build) and [publish](https://python-poetry.org/docs/cli/#publish) commands.
* [Setuptools](https://setuptools.pypa.io/): uses [Bumpversion](https://github.com/peritus/bumpversion) as version management, [build](https://pypa-build.readthedocs.io/) as package builder and [Twine](https://twine.readthedocs.io/) to publish.

The release job is bound to the `publish` stage, appears only on production and integration branches and uses the following variables:

| Name                    | description                                                             | default value     |
| --------------- | ---------------------------------------------------- | ------------- |
| `PYTHON_FORCE_PACKAGE` | Set to `true` to force the packaging even if not on tag related event | _none_ (disabled) |
| ----------------------- | ----------------------------------------------------------------------- | ----------------- |
| `PYTHON_RELEASE_ENABLED`| Set to `true` to enable the release job                                 | _none_ (disabled) |
| `PYTHON_RELEASE_NEXT`   | The part of the version to increase (one of: `major`, `minor`, `patch`) | `minor`           |
| `PYTHON_SEMREL_RELEASE_DISABLED`| Set to `true` to disable [semantic-release integration](#semantic-release-integration)   | _none_ (disabled) |
| `GIT_USERNAME`          | Git username for Git push operations (see below)                        | _none_            |
| :lock: `GIT_PASSWORD`   | Git password for Git push operations (see below)                        | _none_            |
| :lock: `GIT_PRIVATE_KEY`| SSH key for Git push operations (see below)                             | _none_            |
| `PYTHON_REPOSITORY_URL`| Target PyPI repository to publish packages                              | _[GitLab project's PyPI packages repository](https://docs.gitlab.com/ee/user/packages/pypi_repository/)_ |
| `PYTHON_REPOSITORY_USERNAME`| Target PyPI repository username credential                              | `gitlab-ci-token` |
| :lock: `PYTHON_REPOSITORY_PASSWORD`| Target PyPI repository password credential                              | `$CI_JOB_TOKEN` |

#### Setuptools tip

If you're using a `setup.cfg` declarative file for your project Setuptools configuration, then you will have to write a
`.bumpversion.cfg` file to workaround a bug that prevents Bumpversion from updating the project version in your `setup.cfg` file.

Example of `.bumpversion.cfg` file:

```ini
[bumpversion]
# same version as in your setup.cfg
current_version = 0.5.0

[bumpversion:file:setup.cfg]
# any additional config here
# see: https://github.com/peritus/bumpversion#file-specific-configuration
```

### Publish jobs
#### `semantic-release` integration

#### `py-release` job
If you activate the [`semantic-release-info` job from the `semantic-release` template](https://gitlab.com/to-be-continuous/semantic-release/#semantic-release-info-job), the `py-release` job will rely on the generated next version info.
Thus, a release will be performed only if a next semantic release is present.

This job is **disabled by default** and performs an automatic tagging of your Python code.
You should disable the `semantic-release` job (as it's the `py-release` job that will perform the release and so we only need the `semantic-release-info` job) by setting `SEMREL_RELEASE_DISABLED` to `true`.

* [Bumpversion](https://github.com/peritus/bumpversion) Python library is used for version management.
* Looks for an existing `.bumpversion.cfg` at the project root. If found, it will be the configuration used by bumpversion. If not, the `$RELEASE_VERSION_PART` variable and `setup.py` will be used instead.
* Creating a Git tag involves an authenticated and authorized Git user.
Finally, the semantic-release integration can be disabled with the `PYTHON_SEMREL_RELEASE_DISABLED` variable.

**Don't use your personal password !!!
Use an [access token](https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html) with write_repository rights.
If you have a generic account, add it to the project and generate access token from this account.**
#### 

It is bound to the `publish` stage, applies only on master branch and uses the following variables:
#### Git authentication

| Name                   | description                                                             | default value     |
| ---------------------- | ----------------------------------------------------------------------- | ----------------- |
| `RELEASE_VERSION_PART` | The part of the version to increase (one of: `major`, `minor`, `patch`) | `minor`           |
| `RELEASE_USERNAME`     | Username credential for git push                                        | _none_ (disabled) |
| `RELEASE_ACCESS_TOKEN` | Password credential for git push                                        | _none_            |
A Python release involves some Git push operations.

#### `py-publish` job
You can either use a SSH key or user/password credentials.

This job is **disabled by default** and performs a publication of your Python code.
##### Using a SSH key

It is bound to the `publish` stage, applies only on git tags and uses the following variables:
We recommend you to use a [project deploy key](https://docs.gitlab.com/ee/user/project/deploy_keys/#project-deploy-keys) with write access to your project.

| Name                   | description                                              | default value     |
| ---------------------- | -------------------------------------------------------- | ----------------- |
| `PYTHON_PUBLISH_ENABLED`| Set to `true` to enable the publish job                 | _none_ (disabled) |
| `TWINE_REPOSITORY_URL` | Where to publish your Python project                     | GitLab Project's Pypi Packages registry |
| `TWINE_USERNAME`       | Username credential to publish to \$TWINE_REPOSITORY_URL | `gitlab-ci-token` |
| `TWINE_PASSWORD`       | Password credential to publish to \$TWINE_REPOSITORY_URL | `$CI_JOB_TOKEN` |
The key should not have a passphrase (see [how to generate a new SSH key pair](https://docs.gitlab.com/ce/ssh/README.html#generating-a-new-ssh-key-pair)).

More info:
Specify :lock: `$GIT_PRIVATE_KEY` as secret project variable with the private part of the deploy key.

```PEM
-----BEGIN OPENSSH PRIVATE KEY-----
blablabla
-----END OPENSSH PRIVATE KEY-----
```

The template handles both classic variable and file variable.

##### Using user/password credentials

* [Python Packaging User Guide](https://packaging.python.org/)
* [PyPI packages in the Package Registry](https://docs.gitlab.com/ee/user/packages/pypi_repository/)
Simply specify :lock: `$GIT_USERNAME` and :lock: `$GIT_PASSWORD` as secret project variables.

If you want to automatically create tag and publish your Python package, please have a look [here](#release-python)
Note that the password should be an access token (preferably a [project](https://docs.gitlab.com/ee/user/project/settings/project_access_tokens.html) or [group](https://docs.gitlab.com/ee/user/group/settings/group_access_tokens.html) access token) with `read_repository` and `write_repository` scopes.
+35 −45
Original line number Diff line number Diff line
@@ -150,51 +150,14 @@
        }
      ]
    },
    {
      "id": "package",
      "name": "package",
      "description": "Packaging of your Python code",
      "variables": [
        {
          "name": "PYTHON_FORCE_PACKAGE",
          "description": "Force the packaging even if not on tag related event",
          "type": "boolean"
        }
      ]
    },
    {
      "id": "publish",
      "name": "Publish",
      "description": "Publish your code to a [Twine](https://pypi.org/project/twine/) repository",
      "enable_with": "PYTHON_PUBLISH_ENABLED",
      "variables": [
        {
          "name": "TWINE_REPOSITORY_URL",
          "type": "url",
          "description": "Twine repository url to publish you python project",
          "default": "${CI_SERVER_URL}/api/v4/projects/${CI_PROJECT_ID}/packages/pypi"
        },
        {
          "name": "TWINE_USERNAME",
          "description": "Twine repository username credential",
          "secret": true,
          "default": "gitlab-ci-token"
        },
        {
          "name": "TWINE_PASSWORD",
          "description": "Twine repository password credential",
          "secret": true,
          "default": "$CI_JOB_TOKEN"
        }
      ]
    },
    {
      "id": "release",
      "name": "Release",
      "description": "Manually trigger a release of your code (uses [bumpversion](https://pypi.org/project/bumpversion/))",
      "enable_with": "PYTHON_RELEASE_ENABLED",
      "variables": [
        {
          "name": "RELEASE_VERSION_PART",
          "name": "PYTHON_RELEASE_NEXT",
          "type": "enum",
          "values": [
            "",
@@ -207,16 +170,43 @@
          "advanced": true
        },
        {
          "name": "RELEASE_USERNAME",
          "description": "Username credential for Git push",
          "name": "PYTHON_SEMREL_RELEASE_DISABLED",
          "description": "Disable semantic-release integration",
          "type": "boolean",
          "advanced": true
        },
        {
          "name": "GIT_USERNAME",
          "description": "Git username for Git push operations",
          "secret": true
        },
        {
          "name": "GIT_PASSWORD",
          "description": "Git password for Git push operations",
          "secret": true
        },
        {
          "name": "GIT_PRIVATE_KEY",
          "description": "SSH key for Git push operations",
          "secret": true
        },
        {
          "name": "PYTHON_REPOSITORY_URL",
          "type": "url",
          "description": "Target PyPI repository to publish packages.\n\n_defaults to [GitLab project's packages repository](https://docs.gitlab.com/ee/user/packages/pypi_repository/)_",
          "default": "${CI_SERVER_URL}/api/v4/projects/${CI_PROJECT_ID}/packages/pypi"
        },
        {
          "name": "PYTHON_REPOSITORY_USERNAME",
          "description": "Target PyPI repository username credential",
          "secret": true,
          "mandatory": true
          "default": "gitlab-ci-token"
        },
        {
          "name": "RELEASE_ACCESS_TOKEN",
          "description": "Password credential for Git push",
          "name": "PYTHON_REPOSITORY_PASSWORD",
          "description": "Target PyPI repository password credential",
          "secret": true,
          "mandatory": true
          "default": "$CI_JOB_TOKEN"
        }
      ]
    }
+173 −92
Original line number Diff line number Diff line
@@ -46,13 +46,13 @@ variables:
  PYTHON_TRIVY_IMAGE: aquasec/trivy:latest
  PYTHON_TRIVY_ARGS: "--vuln-type library"

  RELEASE_VERSION_PART: "minor"
  PYTHON_RELEASE_NEXT: "minor"

  # By default, publish on the Packages registry of the project
  # https://docs.gitlab.com/ee/user/packages/pypi_repository/#authenticate-with-a-ci-job-token
  TWINE_REPOSITORY_URL: ${CI_SERVER_URL}/api/v4/projects/${CI_PROJECT_ID}/packages/pypi
  TWINE_USERNAME: 'gitlab-ci-token'
  TWINE_PASSWORD: $CI_JOB_TOKEN
  PYTHON_REPOSITORY_URL: ${CI_SERVER_URL}/api/v4/projects/${CI_PROJECT_ID}/packages/pypi
  PYTHON_REPOSITORY_USERNAME: 'gitlab-ci-token'
  PYTHON_REPOSITORY_PASSWORD: $CI_JOB_TOKEN


.python-scripts: &python-scripts |
@@ -360,65 +360,165 @@ variables:
      if ! command -v poetry > /dev/null; then pip install ${PIP_OPTS} poetry; fi
      poetry build
      ;;
    setuptools)
    *)
      # shellcheck disable=SC2086
      pip install ${PIP_OPTS} setuptools build
      pip install ${PIP_OPTS} build
      python -m build
      ;;
    *)
      log_error "--- packaging is unsupported with $PYTHON_BUILD_SYSTEM build system: read template doc"
      exit 1
      ;;
    esac
  }

  function _publish() {
    case "$PYTHON_BUILD_SYSTEM" in
    poetry)
      # shellcheck disable=SC2086
      if ! command -v poetry > /dev/null; then pip install ${PIP_OPTS} poetry; fi
      poetry config repositories.user_defined  "$TWINE_REPOSITORY_URL"
      poetry publish --username "$TWINE_USERNAME" --password "$TWINE_PASSWORD" --repository user_defined
  function configure_scm_auth() {
    git_base_url=$(echo "$CI_REPOSITORY_URL" | cut -d\@ -f2)
    if [[ -n "${GIT_USERNAME}" ]] && [[ -n "${GIT_PASSWORD}" ]]; then
      log_info "--- using https protocol with SCM credentials from env (\$GIT_USERNAME and \$GIT_PASSWORD)..."
      export git_auth_url="https://${GIT_USERNAME}:${GIT_PASSWORD}@${git_base_url}"
    elif [[ -n "${GIT_PRIVATE_KEY}" ]]; then
      log_info "--- using ssh protocol with SSH key from env (\$GIT_PRIVATE_KEY)..."
      mkdir -m 700 "${HOME}/.ssh"
      ssh-keyscan -H "${CI_SERVER_HOST}" >> ~/.ssh/known_hosts
      eval "$(ssh-agent -s)"
      # Handle file variable
      if [[ -f "${GIT_PRIVATE_KEY}" ]]; then
        tr -d '\r' < "${GIT_PRIVATE_KEY}" | ssh-add -
      else
        echo "${GIT_PRIVATE_KEY}" | tr -d '\r' | ssh-add -
      fi
      export git_auth_url="git@${git_base_url/\//:}"
    else
      log_error "--- Please specify either \$GIT_USERNAME and \$GIT_PASSWORD or \$GIT_PRIVATE_KEY variables to enable release (see doc)."
      exit 1
    fi
  }

  function _release() {
    # 0: guess packaging system
    if [[ -f "pyproject.toml" ]]
    then
      # that might be PEP 517 if a build-backend is specified
      # otherwise it might be only used as configuration file for development tools...
      build_backend=$(sed -rn 's/^build-backend *= *"([^"]*)".*/\1/p' pyproject.toml)
      if [[ "$build_backend" ]]
      then
        case "$build_backend" in
        poetry.core.masonry.api)
          log_info "--- Packaging system auto-detected: Poetry"
          pkg_system="poetry"
          ;;
    setuptools)
      # shellcheck disable=SC2086
      pip install ${PIP_OPTS} twine
      twine upload --verbose dist/*.tar.gz
      twine upload --verbose dist/*.whl
        setuptools.build_meta)
          log_info "--- Packaging system auto-detected: Setuptools (PEP 517)"
          pkg_system="setuptools"
          ;;
        *)
      log_error "--- publish is unsupported with $PYTHON_BUILD_SYSTEM build system: read template doc"
          log_error "--- Unsupported PEP 517 backend \\e[33;1m${build_backend}\\e[0m: abort"
          exit 1
          ;;
        esac
  }
      fi
    fi

  function _release() {
    if [[ "${PYTHON_BUILD_SYSTEM}" == "poetry" ]]
    if [[ -z "$pkg_system" ]]
    then
      if [[ -f "setup.py" ]]
      then
        log_info "--- Packaging system auto-detected: Setuptools (legacy)"
        pkg_system="setuptools"
      else
        log_error "--- Couldn't find any supported packaging system: abort"
        exit 1
      fi
    fi

    # 1: retrieve next release info from semantic-release
    if [ "$SEMREL_INFO_ON" ] && [ "$PYTHON_SEMREL_RELEASE_DISABLED" != "true" ]
    then
      if [ -z "$SEMREL_INFO_NEXT_VERSION" ]
      then
        log_info "[semantic-release] no new version to release: skip"
        exit 0
      else
        py_cur_version="$SEMREL_INFO_LAST_VERSION"
        py_next_version="$SEMREL_INFO_NEXT_VERSION"
        py_release_part="$SEMREL_INFO_NEXT_VERSION_TYPE"
        log_info "[semantic-release] new ($py_release_part) release required \\e[1;94m${py_cur_version}\\e[0m → \\e[1;94m${py_next_version}\\e[0m"
      fi
    fi

    # 2: bumpversion (+ Git commit & tag)
    if [[ "$pkg_system" == "poetry" ]]
    then
      # shellcheck disable=SC2086
      if ! command -v poetry > /dev/null; then pip install ${PIP_OPTS} poetry; fi
      poetry version "${RELEASE_VERSION_PART}"
      if [[ -z "$py_next_version" ]]
      then
        py_cur_version=$(poetry version --short)
        py_next_version="$PYTHON_RELEASE_NEXT"
      fi
      log_info "[Poetry] change version \\e[1;94m${py_cur_version}\\e[0m → \\e[1;94m${py_next_version}\\e[0m"
      poetry version ${TRACE+--verbose} "$py_next_version"
      # eval exact next version
      py_next_version=$(poetry version --short)
      git add pyproject.toml
      git commit -m "chore(python-release): $py_cur_version → $py_next_version [ci skip]"
      git tag "$py_next_version"
    else
      # Setuptools / bumpversion
      # shellcheck disable=SC2086
      pip install ${PIP_OPTS} bumpversion

      if [[ -f ".bumpversion.cfg" ]]; then
        log_info "--- .bumpversion.cfg file found "
        export bumpversion_args="${RELEASE_VERSION_PART} --verbose"
      else
        log_info "---  No .bumpversion.cfg file found "
        if [[ -f "setup.py" ]]; then
          log_info "---  Getting current version of setup.py file "
          current_version=$(python setup.py --version)
          export bumpversion_args=" --verbose --current-version ${current_version} --tag --tag-name {new_version} --commit ${RELEASE_VERSION_PART} setup.py"
      py_commit_message="chore(python-release): {current_version} → {new_version} [ci skip]"
      if [[ "$py_next_version" ]]
      then
        # explicit release version (semantic-release)
        log_info "[Setuptools] bumpversion \\e[1;94m${py_cur_version}\\e[0m → \\e[1;94m${py_next_version}\\e[0m"
        # create cfg in case it doesn't exist - will be updated by bumpversion
        touch .bumpversion.cfg
        bumpversion ${TRACE+--verbose} --current-version "$py_cur_version" --commit --message "$py_commit_message" --tag --tag-name "{new_version}" "$py_release_part"
      elif [[ -f "setup.py" ]]
      then
        # retrieve current version from setup.py
        py_cur_version=$(python setup.py --version)
        py_release_part="$PYTHON_RELEASE_NEXT"
        log_info "[Setuptools] bumpversion ($py_release_part) from \\e[1;94m${py_cur_version}\\e[0m"
        bumpversion ${TRACE+--verbose} --current-version "$py_cur_version" --commit --message "$py_commit_message" --tag --tag-name "{new_version}" "$py_release_part"
      elif [[ -f ".bumpversion.cfg" ]]
      then
        # current version shall be set in .bumpversion.cfg
        py_release_part="$PYTHON_RELEASE_NEXT"
        log_info "[bumpversion] increase \\e[1;94m${py_release_part}\\e[0m"
        bumpversion ${TRACE+--verbose} --commit --message "$py_commit_message" --tag --tag-name "{new_version}" "$py_release_part"
      else
          log_warn "---  No setup.py file found. Cannot perform release."
        log_error "--- setup.py or .bumpversion.cfg file required to retrieve current version: cannot perform release"
        exit 1
      fi
    fi
      log_info "--- Release args: ${bumpversion_args}"

      bumpversion "${bumpversion_args}"
    # 3: Git commit, tag and push
    log_info "--- git push commit and tag..."
    git push "$git_auth_url" "$CI_BUILD_REF_NAME"
    git push "$git_auth_url" --tags

    # 4: build new version distribution
    log_info "--- build distribution packages..."
    if [[ "$pkg_system" == "poetry" ]]
    then
      poetry build ${TRACE+--verbose}
    else
      # shellcheck disable=SC2086
      pip install ${PIP_OPTS} build
      rm -rf dist
      python -m build
    fi

    # 5: publish packages
    log_info "--- publish distribution packages..."
    if [[ "$pkg_system" == "poetry" ]]
    then
      poetry config repositories.user_defined  "$PYTHON_REPOSITORY_URL"
      poetry publish ${TRACE+--verbose} --username "$PYTHON_REPOSITORY_USERNAME" --password "$PYTHON_REPOSITORY_PASSWORD" --repository user_defined
    else
      # shellcheck disable=SC2086
      pip install ${PIP_OPTS} twine
      twine upload ${TRACE+--verbose} --username "$PYTHON_REPOSITORY_USERNAME" --password "$PYTHON_REPOSITORY_PASSWORD" --repository-url "$PYTHON_REPOSITORY_URL" dist/*
    fi
  }

@@ -473,12 +573,26 @@ variables:
stages:
  - build
  - test
  - package-build
  - publish

###############################################################################################
#                                      build stage                                             #
###############################################################################################
# build Python packages as artifacts
py-package:
  extends: .python-base
  stage: build
  script:
    - _package
  artifacts:
    paths:
      - $PYTHON_PROJECT_DIR/dist/*
  rules:
    # exclude merge requests
    - if: $CI_MERGE_REQUEST_ID
      when: never
    - if: '$PYTHON_PACKAGE_ENABLED == "true"'

py-lint:
  extends: .python-base
  stage: build
@@ -713,7 +827,6 @@ py-trivy:
      fi
      trivy fs ${PYTHON_TRIVY_ARGS} --format table --exit-code 0 $PYTHON_PROJECT_DIR
      trivy fs ${PYTHON_TRIVY_ARGS} --format json --output reports/trivy-python.json --exit-code 1 $PYTHON_PROJECT_DIR
  
  artifacts:
    name: "$CI_JOB_NAME artifacts from $CI_PROJECT_NAME on $CI_COMMIT_REF_SLUG"
    expire_in: 1 day
@@ -735,62 +848,30 @@ py-trivy:
      when: manual
      allow_failure: true

###############################################################################################
#                                      package stage                                           #
###############################################################################################

# (on tag creation): create packages as artifacts
py-package:
  extends: .python-base
  stage: package-build
  script:
    - _package
  artifacts:
    paths:
      - $PYTHON_PROJECT_DIR/dist/*.tar.gz
      - $PYTHON_PROJECT_DIR/dist/*.whl
  rules:
    # on tags
    - if: '$CI_COMMIT_TAG'
    - if: '$PYTHON_FORCE_PACKAGE == "true"'

###############################################################################################
#                                      publish stage                                           #
###############################################################################################

# (on tag creation): performs a release
py-publish:
  extends: .python-base
  stage: publish
  script:
    - assert_defined "$TWINE_USERNAME" 'Missing required env $TWINE_USERNAME'
    - assert_defined "$TWINE_PASSWORD" 'Missing required env $TWINE_PASSWORD'
    - _publish
  rules:
    # on tags with $PYTHON_PUBLISH_ENABLED set
    - if: '$PYTHON_PUBLISH_ENABLED == "true" && $CI_COMMIT_TAG'

# (manual from master branch): triggers a release (tag creation)
py-release:
  extends: .python-base
  stage: publish
  script:
    - git config --global user.email '$GITLAB_USER_EMAIL'
    - git config --global user.name '$GITLAB_USER_LOGIN'
    - git config --global user.email "$GITLAB_USER_EMAIL"
    - git config --global user.name "$GITLAB_USER_LOGIN"
    - git checkout -B $CI_BUILD_REF_NAME
    - configure_scm_auth
    - _release
    - git_url_base=`echo ${CI_REPOSITORY_URL} | cut -d\@ -f2`
    - git push https://${RELEASE_USERNAME}:${RELEASE_ACCESS_TOKEN}@${git_url_base} --tags
    - git push https://${RELEASE_USERNAME}:${RELEASE_ACCESS_TOKEN}@${git_url_base} $CI_BUILD_REF_NAME
  artifacts:
    paths:
      - $PYTHON_PROJECT_DIR/dist/*
  rules:
    # exclude merge requests
    - if: $CI_MERGE_REQUEST_ID
      when: never
    # on production branch(es): manual & non-blocking if $RELEASE_USERNAME is set
    - if: '$RELEASE_USERNAME && $CI_COMMIT_REF_NAME =~ $PROD_REF'
      when: manual
      allow_failure: true
    # on integration branch(es): manual & non-blocking if $RELEASE_USERNAME is set
    - if: '$RELEASE_USERNAME && $CI_COMMIT_REF_NAME =~ $INTEG_REF'
    # exclude if $PYTHON_RELEASE_ENABLED not set
    - if: '$PYTHON_RELEASE_ENABLED != "true"'
      when: never
    # exclude on non-prod, non-integ branches
    - if: '$CI_COMMIT_REF_NAME !~ $PROD_REF && $CI_COMMIT_REF_NAME !~ $INTEG_REF'
      when: never
    # else: manual
    - if: '$PYTHON_RELEASE_ENABLED == "true"' # useless but prevents GitLab warning
      when: manual
      allow_failure: true