diff --git a/.gitlab/merge_request_templates/new_feature.md b/.gitlab/merge_request_templates/new_feature.md index 74abae94c94dc0768bb5c51fe51ad253fce113fe..491b7f98ded7e0da03d18c95978eafcb7d86619f 100644 --- a/.gitlab/merge_request_templates/new_feature.md +++ b/.gitlab/merge_request_templates/new_feature.md @@ -8,8 +8,8 @@ Closes #999 ## Checklist * General: - * [ ] use [rules](https://docs.gitlab.com/ee/ci/yaml/#rules) instead of [only/except](https://docs.gitlab.com/ee/ci/yaml/#onlyexcept-advanced) - * [ ] optimized [cache](https://docs.gitlab.com/ee/ci/caching/) configuration (wherever applicable) + * [ ] use [rules](https://docs.gitlab.com/ci/yaml/#rules) instead of [only/except](https://docs.gitlab.com/ci/yaml/#onlyexcept-advanced) + * [ ] optimized [cache](https://docs.gitlab.com/ci/caching/) configuration (wherever applicable) * Publicly usable: * [ ] untagged runners * [ ] no proxy configuration but support `http_proxy`/`https_proxy`/`no_proxy` diff --git a/CHANGELOG.md b/CHANGELOG.md index e1faff804e89c152860b763071bb1a79225cba71..80e65e82ef61dea84bd442927e54b1658df25026 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,69 @@ -# [7.8.0](https://git.code.tecnalia.dev/smartdatalab/public/ci-cd-components/python/compare/7.7.1...7.8.0) (2025-01-29) +## [7.10.2](https://gitlab.com/to-be-continuous/python/compare/7.10.1...7.10.2) (2025-05-03) + + +### Bug Fixes + +* add python cmd when python3 is present ([e6c8d7f](https://gitlab.com/to-be-continuous/python/commit/e6c8d7f98cd785d2a4ccaf777a9c3d0016a3da19)) + +## [7.10.1](https://gitlab.com/to-be-continuous/python/compare/7.10.0...7.10.1) (2025-05-02) + + +### Bug Fixes + +* change to pytest bin instead of module ([19be433](https://gitlab.com/to-be-continuous/python/commit/19be433bf16097a98ae5de4f633ebd9fe807e4ef)), closes [#109](https://gitlab.com/to-be-continuous/python/issues/109) +* exclude venv on py-lint ([d459124](https://gitlab.com/to-be-continuous/python/commit/d45912485cd7e8a1d802dda72eca6b6bfe1860b8)) +* py-package remove reports dir ([300d31f](https://gitlab.com/to-be-continuous/python/commit/300d31f51e6670cc6f09d6af01531b77e9d270af)), closes [#98](https://gitlab.com/to-be-continuous/python/issues/98) + +# [7.10.0](https://gitlab.com/to-be-continuous/python/compare/7.9.2...7.10.0) (2025-04-16) + + +### Features + +* **Hatch:** add Hatch support as a new build system ([f684e63](https://gitlab.com/to-be-continuous/python/commit/f684e634496711d984843b25141f57df6e3826be)) + +## [7.9.2](https://gitlab.com/to-be-continuous/python/compare/7.9.1...7.9.2) (2025-04-02) + + +### Bug Fixes + +* **sbom:** disable file catalogers for Syft SBOM (to minimize SBOM file) ([d83edb0](https://gitlab.com/to-be-continuous/python/commit/d83edb06767741edd400ed195981df778414e9cd)) + +## [7.9.1](https://gitlab.com/to-be-continuous/python/compare/7.9.0...7.9.1) (2025-03-11) + + +### Bug Fixes + +* **bump-my-version:** improve bump-my-version config verification (solves [#106](https://gitlab.com/to-be-continuous/python/issues/106)) ([64b624a](https://gitlab.com/to-be-continuous/python/commit/64b624a4d0abde429d50a00a9c595993c369fbd0)) + +# [7.9.0](https://gitlab.com/to-be-continuous/python/compare/7.8.3...7.9.0) (2025-03-10) + + +### Features + +* skip GCP ADC authent when GCP_JWT is not present ([b43207f](https://gitlab.com/to-be-continuous/python/commit/b43207f6eee26a8d17bc75ed19b54208534b3ad9)) + +## [7.8.3](https://gitlab.com/to-be-continuous/python/compare/7.8.2...7.8.3) (2025-02-23) + + +### Bug Fixes + +* change _pip to pass cmd then PIP_OPTS ([c1b277e](https://gitlab.com/to-be-continuous/python/commit/c1b277e31b977b41eedd5e213e7672d11c66da33)) + +## [7.8.2](https://gitlab.com/to-be-continuous/python/compare/7.8.1...7.8.2) (2025-02-03) + + +### Bug Fixes + +* **gcp:** reduce scope of GCP App Default Creds script to template ([829bfce](https://gitlab.com/to-be-continuous/python/commit/829bfceffe3a2e097914c719d4a4488d544be7ab)) + +## [7.8.1](https://gitlab.com/to-be-continuous/python/compare/7.8.0...7.8.1) (2025-01-31) + + +### Bug Fixes + +* **sbom:** only generate SBOMs on prod branches, integ branches and release tags ([8da756f](https://gitlab.com/to-be-continuous/python/commit/8da756f273cb22dbd12c866ba1e6f7f07b52cb4a)) + +# [7.8.0](https://gitlab.com/to-be-continuous/python/compare/7.7.1...7.8.0) (2025-01-27) ### Features diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 61bf40af56b1cf41088143fc212889d847b11ae7..be729277830ae90e789a9a0bbd8d2b38889bc15d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -61,7 +61,7 @@ To contribute: 1. Create an issue describing the bug or enhancement you want to propose (select the right issue template). 2. Make sure the issue has been reviewed and agreed. -3. Create a Merge Request, from your **own** fork (see [forking workflow](https://docs.gitlab.com/ee/user/project/repository/forking_workflow.html) documentation). +3. Create a Merge Request, from your **own** fork (see [forking workflow](https://docs.gitlab.com/user/project/repository/forking_workflow/) documentation). Don't hesitate to mark your MR as `Draft` as long as you think it's not ready to be reviewed. ### Git Commit Conventions diff --git a/README.md b/README.md index 8375c603d802a7fc9155b49087a7513a6602f951..0826f446643a8f85775211f28cfbed055e3f9105 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,8 @@ This project implements a GitLab CI/CD template to build, test and analyse your ## Usage -This template can be used both as a [CI/CD component](https://docs.gitlab.com/ee/ci/components/#use-a-component) -or using the legacy [`include:project`](https://docs.gitlab.com/ee/ci/yaml/index.html#includeproject) syntax. +This template can be used both as a [CI/CD component](https://docs.gitlab.com/ci/components/#use-a-component) +or using the legacy [`include:project`](https://docs.gitlab.com/ci/yaml/#includeproject) syntax. ### Use as a CI/CD component @@ -14,7 +14,7 @@ Add the following to your `.gitlab-ci.yml`: ```yaml include: # 1: include the component - - component: $CI_SERVER_FQDN/to-be-continuous/python/gitlab-ci-python@7.8.0 + - component: $CI_SERVER_FQDN/to-be-continuous/python/gitlab-ci-python@7.10.2 # 2: set/override component inputs inputs: image: registry.hub.docker.com/library/python:3.12-slim @@ -29,7 +29,7 @@ Add the following to your `.gitlab-ci.yml`: include: # 1: include the template - project: 'to-be-continuous/python' - ref: '7.8.0' + ref: '7.10.2' file: '/templates/gitlab-ci-python.yml' variables: @@ -44,13 +44,13 @@ The Python template uses some global configuration used throughout all jobs. | Input / Variable | Description | Default value | | -------------------- | ------------------------------------------------------------------------------------- | ------------------ | -| `image` / `PYTHON_IMAGE` | The Docker image used to run Python <br/>:warning: **set the version required by your project** | `registry.hub.docker.com/library/python:3-slim` | +| `image` / `PYTHON_IMAGE` | The Docker image used to run Python <br/>:warning: **set the version required by your project** | `registry.hub.docker.com/library/python:3-slim` <br/>[](https://to-be-continuous.gitlab.io/doc/secu/trivy-PYTHON_IMAGE) | | `project-dir` / `PYTHON_PROJECT_DIR` | Python project root directory | `.` | | `build-system` / `PYTHON_BUILD_SYSTEM`| Python build-system to use to install dependencies, build and package the project (see below) | `auto` (auto-detect) | | `PIP_INDEX_URL` | Python repository url | _none_ | | `PIP_EXTRA_INDEX_URL` | Extra Python repository url | _none_ | | `pip-opts` / `PIP_OPTS` | pip [extra options](https://pip.pypa.io/en/stable/cli/pip/#general-options) | _none_ | -| `extra-deps` / `PYTHON_EXTRA_DEPS` | Python extra sets of dependencies to install<br/>For [Setuptools](https://setuptools.pypa.io/en/latest/userguide/dependency_management.html?highlight=extras#optional-dependencies) or [Poetry](https://python-poetry.org/docs/pyproject/#extras) only | _none_ | +| `extra-deps` / `PYTHON_EXTRA_DEPS` | Python extra sets of dependencies to install<br/>For [Setuptools](https://setuptools.pypa.io/en/latest/userguide/dependency_management.html?highlight=extras#optional-dependencies) or [Poetry](https://python-poetry.org/docs/pyproject/#extras) or [uv](https://docs.astral.sh/uv/) only | _none_ | | `reqs-file` / `PYTHON_REQS_FILE` | Main requirements file _(relative to `$PYTHON_PROJECT_DIR`)_<br/>For [Requirements Files](https://pip.pypa.io/en/stable/user_guide/#requirements-files) build-system only | `requirements.txt` | | `extra-reqs-files` / `PYTHON_EXTRA_REQS_FILES` | Extra dev requirements file(s) to install _(relative to `$PYTHON_PROJECT_DIR`)_ | `requirements-dev.txt` | | `py-publish-job-tags` / `PY_PUBLISH_JOB_TAGS` | Tags to be used for selecting runners for the job | `[]` | @@ -67,15 +67,58 @@ and/or `setup.py` and/or `requirements.txt`), but the build system might also be | Value | Build System (scope) | | ---------------- |--------------------------------------------------------------------------------------------------------| -| _none_ (default) or `auto` | The template tries to **auto-detect** the actual build system | -| `setuptools` | [Setuptools](https://setuptools.pypa.io/) (dependencies, build & packaging) | -| `poetry` | [Poetry](https://python-poetry.org/) (dependencies, build, test & packaging) | -| `uv` | [uv](https://docs.astral.sh/uv/) (dependencies, build, test & packaging)| -| `pipenv` | [Pipenv](https://pipenv.pypa.io/) (dependencies only) | -| `reqfile` | [Requirements Files](https://pip.pypa.io/en/stable/user_guide/#requirements-files) (dependencies only) | +| _none_ (default) or `auto` | The template tries to **auto-detect** :sparkles: the actual build system | +| `setuptools` | [](https://setuptools.pypa.io/)  | +| `poetry` | [](https://python-poetry.org/)  | +| `uv` | [](https://docs.astral.sh/uv/)  | +| `hatch` | [](https://hatch.pypa.io/latest/)  | +| `pipenv` | [](https://pipenv.pypa.io/)  | +| `reqfile` | [](https://pip.pypa.io/en/stable/user_guide/#requirements-files)  | :warning: You can explicitly set the build tool version by setting `$PYTHON_BUILD_SYSTEM` variable including a [version identification](https://peps.python.org/pep-0440/). For example `PYTHON_BUILD_SYSTEM="poetry==1.1.15"` +### Hatch + +All template jobs use the `default` Hatch environment. So dev dependencies should defined in the `[tool.hatch.envs.default.dependencies]` section of `pyproject.toml`. + +```toml +[tool.hatch.envs.default] +dependencies = [ + "pytest>=8.0.0,<9", +... +] +``` + +## Use Multiple Python Versions + +For some jobs, it can be relevant to use multiple Python versions, such as: + +- Test jobs: `py-unittest`, `py-pytest`, `py-nosetest` +- `py-publish` jobs (especially if you're not generating a pure Python package) + +This setup is done by defining the PYTHON_IMAGE variable with a parallel/matrix strategy in your .gitlab-ci.yml. + +For example, to run py-test jobs using both python:3.13-slim and python:3.12-alpine: + +```yaml +py-pytest: + parallel: + matrix: + - PYTHON_IMAGE: python:3.13-slim + - PYTHON_IMAGE: python:3.12-alpine +``` + +If your tests cannot be executed concurrently due to shared resources (e.g. database access), you can use the `resource_group` feature to limit parallel execution: + +```yaml +py-pytest: + parallel: + matrix: + - PYTHON_IMAGE: python:3.13-slim + - PYTHON_IMAGE: python:3.12-alpine + resource_group: db_access_tests +``` + ## Jobs ### `py-package` job @@ -88,6 +131,13 @@ It is bound to the `build` stage, it is **disabled by default** and can be enabl | ------------------------ | ---------------------------------- | ----------------- | | `py-package-job-tags` / `PY_PACKAGE_JOB_TAGS` | Tags to be used for selecting runners for the job | `[]` | +#### UV tip + +Currently, UV supports the following underlying build systems: + +- [hatchling](https://pypi.org/project/hatchling/) (this is the default setting when creating a new project with UV), so you may use the same build configuration as [Hatch](https://hatch.pypa.io/latest/build/). The recommended file layout is [`src-layout`](https://packaging.python.org/en/latest/discussions/src-layout-vs-flat-layout/) to simplify [finding the source](https://hatch.pypa.io/latest/plugins/builder/wheel/#default-file-selection) otherwise configure [Hatch build config](https://hatch.pypa.io/latest/config/build/). +- or [setuptools](https://pypi.org/project/setuptools/) (this is the default if nothing is defined in your `pyproject.toml`). In this case, the recommended file layout is also [`src-layout`](https://packaging.python.org/en/latest/discussions/src-layout-vs-flat-layout/), otherwise configure [`package_discovery`](https://setuptools.pypa.io/en/latest/userguide/package_discovery.html). + ### Lint jobs #### `py-lint` job @@ -108,7 +158,7 @@ In addition to a textual report in the console, this job produces the following | Report | Format | Usage | | -------------- | ---------------------------------------------------------------------------- | ----------------- | -| `$PYTHON_PROJECT_DIR/reports/py-lint.codeclimate.json` | [Code Climate](https://docs.codeclimate.com/docs/pylint) | [GitLab integration](https://docs.gitlab.com/ee/ci/yaml/artifacts_reports.html#artifactsreportscodequality) | +| `$PYTHON_PROJECT_DIR/reports/py-lint.codeclimate.json` | [Code Climate](https://docs.codeclimate.com/docs/pylint) | [GitLab integration](https://docs.gitlab.com/ci/yaml/artifacts_reports/#artifactsreportscodequality) | | `$PYTHON_PROJECT_DIR/reports/py-lint.parseable.txt` | [parseable](https://pylint.pycqa.org/en/latest/user_guide/usage/output.html) | [SonarQube integration](https://docs.sonarsource.com/sonarqube-server/latest/analyzing-source-code/importing-external-issues/external-analyzer-reports/) | ### Test jobs @@ -153,8 +203,8 @@ In addition to a textual report in the console, this job produces the following | Report | Format | Usage | | -------------- | ---------------------------------------------------------------------------- | ----------------- | -| `$PYTHON_PROJECT_DIR/reports/TEST-*.xml` | [xUnit](https://en.wikipedia.org/wiki/XUnit) test report(s) | [GitLab integration](https://docs.gitlab.com/ee/ci/yaml/artifacts_reports.html#artifactsreportsjunit) & [SonarQube integration](https://docs.sonarsource.com/sonarqube-server/latest/analyzing-source-code/test-coverage/test-execution-parameters/#python) | -| `$PYTHON_PROJECT_DIR/reports/py-coverage.cobertura.xml` | [Cobertura XML](https://gcovr.com/en/stable/output/cobertura.html) coverage report | [GitLab integration](https://docs.gitlab.com/ee/ci/yaml/artifacts_reports.html#artifactsreportscoverage_report) & [SonarQube integration](https://docs.sonarsource.com/sonarqube-server/latest/analyzing-source-code/test-coverage/python-test-coverage/) | +| `$PYTHON_PROJECT_DIR/reports/TEST-*.xml` | [xUnit](https://en.wikipedia.org/wiki/XUnit) test report(s) | [GitLab integration](https://docs.gitlab.com/ci/yaml/artifacts_reports/#artifactsreportsjunit) & [SonarQube integration](https://docs.sonarsource.com/sonarqube-server/latest/analyzing-source-code/test-coverage/test-execution-parameters/#python) | +| `$PYTHON_PROJECT_DIR/reports/py-coverage.cobertura.xml` | [Cobertura XML](https://gcovr.com/en/stable/output/cobertura.html) coverage report | [GitLab integration](https://docs.gitlab.com/ci/yaml/artifacts_reports/#artifactsreportscoverage_report) & [SonarQube integration](https://docs.sonarsource.com/sonarqube-server/latest/analyzing-source-code/test-coverage/python-test-coverage/) | #### `py-pytest` job @@ -187,8 +237,8 @@ In addition to a textual report in the console, this job produces the following | Report | Format | Usage | | -------------- | ---------------------------------------------------------------------------- | ----------------- | -| `$PYTHON_PROJECT_DIR/reports/TEST-*.xml` | [xUnit](https://en.wikipedia.org/wiki/XUnit) test report(s) | [GitLab integration](https://docs.gitlab.com/ee/ci/yaml/artifacts_reports.html#artifactsreportsjunit) & [SonarQube integration](https://docs.sonarsource.com/sonarqube-server/latest/analyzing-source-code/test-coverage/test-execution-parameters/#python) | -| `$PYTHON_PROJECT_DIR/reports/py-coverage.cobertura.xml` | [Cobertura XML](https://gcovr.com/en/stable/output/cobertura.html) coverage report | [GitLab integration](https://docs.gitlab.com/ee/ci/yaml/artifacts_reports.html#artifactsreportscoverage_report) & [SonarQube integration](https://docs.sonarsource.com/sonarqube-server/latest/analyzing-source-code/test-coverage/python-test-coverage/) | +| `$PYTHON_PROJECT_DIR/reports/TEST-*.xml` | [xUnit](https://en.wikipedia.org/wiki/XUnit) test report(s) | [GitLab integration](https://docs.gitlab.com/ci/yaml/artifacts_reports/#artifactsreportsjunit) & [SonarQube integration](https://docs.sonarsource.com/sonarqube-server/latest/analyzing-source-code/test-coverage/test-execution-parameters/#python) | +| `$PYTHON_PROJECT_DIR/reports/py-coverage.cobertura.xml` | [Cobertura XML](https://gcovr.com/en/stable/output/cobertura.html) coverage report | [GitLab integration](https://docs.gitlab.com/ci/yaml/artifacts_reports/#artifactsreportscoverage_report) & [SonarQube integration](https://docs.sonarsource.com/sonarqube-server/latest/analyzing-source-code/test-coverage/python-test-coverage/) | #### `py-nosetests` job @@ -212,8 +262,8 @@ In addition to a textual report in the console, this job produces the following | Report | Format | Usage | | -------------- | ---------------------------------------------------------------------------- | ----------------- | -| `$PYTHON_PROJECT_DIR/reports/TEST-*.xml` | [xUnit](https://en.wikipedia.org/wiki/XUnit) test report(s) | [GitLab integration](https://docs.gitlab.com/ee/ci/yaml/artifacts_reports.html#artifactsreportsjunit) & [SonarQube integration](https://docs.sonarsource.com/sonarqube-server/latest/analyzing-source-code/test-coverage/test-execution-parameters/#python) | -| `$PYTHON_PROJECT_DIR/reports/py-coverage.cobertura.xml` | [Cobertura XML](https://gcovr.com/en/stable/output/cobertura.html) coverage report | [GitLab integration](https://docs.gitlab.com/ee/ci/yaml/artifacts_reports.html#artifactsreportscoverage_report) & [SonarQube integration](https://docs.sonarsource.com/sonarqube-server/latest/analyzing-source-code/test-coverage/python-test-coverage/) | +| `$PYTHON_PROJECT_DIR/reports/TEST-*.xml` | [xUnit](https://en.wikipedia.org/wiki/XUnit) test report(s) | [GitLab integration](https://docs.gitlab.com/ci/yaml/artifacts_reports/#artifactsreportsjunit) & [SonarQube integration](https://docs.sonarsource.com/sonarqube-server/latest/analyzing-source-code/test-coverage/test-execution-parameters/#python) | +| `$PYTHON_PROJECT_DIR/reports/py-coverage.cobertura.xml` | [Cobertura XML](https://gcovr.com/en/stable/output/cobertura.html) coverage report | [GitLab integration](https://docs.gitlab.com/ci/yaml/artifacts_reports/#artifactsreportscoverage_report) & [SonarQube integration](https://docs.sonarsource.com/sonarqube-server/latest/analyzing-source-code/test-coverage/python-test-coverage/) | #### `py-compile` job @@ -286,16 +336,17 @@ It is bound to the `test` stage, and uses the following variables: | Input / Variable | Description | Default value | | --------------------- | -------------------------------------- | ----------------- | | `sbom-disabled` / `PYTHON_SBOM_DISABLED` | Set to `true` to disable this job | _none_ | +| `TBC_SBOM_MODE` | Controls when SBOM reports are generated (`onrelease`: only on `$INTEG_REF`, `$PROD_REF` and `$RELEASE_REF` pipelines; `always`: any pipeline).<br/>:warning: `sbom-disabled` / `PYTHON_SBOM_DISABLED` takes precedence | `onrelease` | | `sbom-syft-url` / `PYTHON_SBOM_SYFT_URL` | Url to the `tar.gz` package for `linux_amd64` of Syft to use (ex: `https://github.com/anchore/syft/releases/download/v0.62.3/syft_0.62.3_linux_amd64.tar.gz`)<br/>_When unset, the latest version will be used_ | _none_ | | `sbom-name` / `PYTHON_SBOM_NAME` | Component name of the emitted SBOM | `$CI_PROJECT_PATH/$PYTHON_PROJECT_DIR` | -| `sbom-opts` / `PYTHON_SBOM_OPTS` | Options for syft used for SBOM analysis | `--override-default-catalogers python-package-cataloger` | +| `sbom-opts` / `PYTHON_SBOM_OPTS` | Options for syft used for SBOM analysis | `--override-default-catalogers python-package-cataloger --select-catalogers -file` | | `py-sbom-job-tags` / `PY_SBOM_JOB_TAGS` | Tags to be used for selecting runners for the job | `[]` | In addition to logs in the console, this job produces the following reports, kept for one week: | Report | Format | Usage | | -------------- | ---------------------------------------------------------------------------- | ----------------- | -| `$PYTHON_PROJECT_DIR/reports/py-sbom.cyclonedx.json` | [CycloneDX JSON](https://cyclonedx.org/docs/latest/json/) | [Security & Compliance integration](https://docs.gitlab.com/ee/ci/yaml/artifacts_reports.html#artifactsreportscyclonedx) | +| `$PYTHON_PROJECT_DIR/reports/py-sbom.cyclonedx.json` | [CycloneDX JSON](https://cyclonedx.org/docs/latest/json/) | [Security & Compliance integration](https://docs.gitlab.com/ci/yaml/artifacts_reports/#artifactsreportscyclonedx) | ### `py-black` job @@ -331,7 +382,7 @@ In addition to logs in the console, this job produces the following reports, kep | Report | Format | Usage | | -------------- | ---------------------------------------------------------------------------- | ----------------- | -| `$PYTHON_PROJECT_DIR/reports/py-ruff.gitlab.json` | [GitLab](https://docs.astral.sh/ruff/settings/#output-format) | [GitLab integration](https://docs.gitlab.com/ee/ci/yaml/artifacts_reports.html#artifactsreportscodequality) | +| `$PYTHON_PROJECT_DIR/reports/py-ruff.gitlab.json` | [GitLab](https://docs.astral.sh/ruff/settings/#output-format) | [GitLab integration](https://docs.gitlab.com/ci/yaml/artifacts_reports/#artifactsreportscodequality) | | `$PYTHON_PROJECT_DIR/reports/py-ruff.native.json` | [JSON](https://docs.astral.sh/ruff/settings/#output-format) | [SonarQube integration](https://docs.sonarsource.com/sonarqube-server/latest/analyzing-source-code/importing-external-issues/external-analyzer-reports/)<br/>_This report is generated only if SonarQube template is detected_ | ### `py-ruff-format` job @@ -362,7 +413,7 @@ In addition to a textual report in the console, this job produces the following | Report | Format | Usage | | -------------- | ---------------------------------------------------------------------------- | ----------------- | -| `$PYTHON_PROJECT_DIR/reports/py-mypy.codeclimate.json` | [Code Climate](https://github.com/soul-catcher/mypy-gitlab-code-quality) | [GitLab integration](https://docs.gitlab.com/ee/ci/yaml/artifacts_reports.html#artifactsreportscodequality) | +| `$PYTHON_PROJECT_DIR/reports/py-mypy.codeclimate.json` | [Code Climate](https://github.com/soul-catcher/mypy-gitlab-code-quality) | [GitLab integration](https://docs.gitlab.com/ci/yaml/artifacts_reports/#artifactsreportscodequality) | | `$PYTHON_PROJECT_DIR/reports/py-mypy.console.txt` | [mypy console output](https://mypy.readthedocs.io/) | [SonarQube integration](https://docs.sonarsource.com/sonarqube-server/latest/analyzing-source-code/importing-external-issues/external-analyzer-reports/) | ### SonarQube analysis @@ -407,12 +458,13 @@ This job is **disabled by default** and allows to perform a complete release of 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). +4. publish the built packages to a PyPI compatible repository ([GitLab packages](https://docs.gitlab.com/user/packages/pypi_repository/) by default). The Python template supports three packaging systems: * [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. -* [uv](https://docs.astral.sh/uv/): uses [uv](https://docs.astral.sh/uv/) as version management, [build](https://docs.astral.sh/uv/guides/publish/#building-your-package) as package builder and [publish](https://docs.astral.sh/uv/guides/publish/) to publish. +* [uv](https://docs.astral.sh/uv/): uses [bump-my-version](https://github.com/callowayproject/bump-my-version) as version management, [build](https://docs.astral.sh/uv/guides/publish/#building-your-package) as package builder and [publish](https://docs.astral.sh/uv/guides/publish/) to publish. +* [hatch](https://hatch.pypa.io/latest/): uses [bump-my-version](https://github.com/callowayproject/bump-my-version) as version management, [build](https://hatch.pypa.io/latest/build/) as package builder and [publish](https://hatch.pypa.io/latest/publish/) to publish. * [Setuptools](https://setuptools.pypa.io/): uses [bump-my-version](https://github.com/callowayproject/bump-my-version) 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: @@ -432,12 +484,13 @@ When `py-release` job is enabled, `py-publish` job is automatically enabled too. ### `py-publish` job -This job is **disabled by default** and allow to 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 allow to publish the built packages to a PyPI compatible repository ([GitLab packages](https://docs.gitlab.com/user/packages/pypi_repository/) by default. The Python template supports three packaging systems: * [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. -* [uv](https://docs.astral.sh/uv/): uses [uv](https://docs.astral.sh/uv/) as version management, [build](https://docs.astral.sh/uv/guides/publish/#building-your-package) as package builder and [publish](https://docs.astral.sh/uv/guides/publish/) to publish. +* [uv](https://docs.astral.sh/uv/): uses [bump-my-version](https://github.com/callowayproject/bump-my-version) as version management, [build](https://docs.astral.sh/uv/guides/publish/#building-your-package) as package builder and [publish](https://docs.astral.sh/uv/guides/publish/) to publish. +* [hatch](https://hatch.pypa.io/latest//): uses [bump-my-version](https://github.com/callowayproject/bump-my-version) as version management, [build](https://hatch.pypa.io/latest/build/) as package builder and [publish](https://hatch.pypa.io/latest/publish/) to publish. * [Setuptools](https://setuptools.pypa.io/): uses [bump-my-version](https://github.com/callowayproject/bump-my-version) as version management, [build](https://pypa-build.readthedocs.io/) as package builder and [Twine](https://twine.readthedocs.io/) to publish. The publish job is bound to the `publish` stage, is executed on a Git tag matching [semantic versioning pattern](https://semver.org/) and uses the following variables: @@ -445,11 +498,12 @@ The publish job is bound to the `publish` stage, is executed on a Git tag matchi | Input / Variable | Description | Default value | | ----------------------- | ----------------------------------------------------------------------- | ----------------- | | `publish-enabled` / `PYTHON_PUBLISH_ENABLED`| Set to `true` to enable the publish job | _none_ (disabled) | -| `repository-url` / `PYTHON_REPOSITORY_URL`| Target PyPI repository to publish packages | _[GitLab project's PyPI packages repository](https://docs.gitlab.com/ee/user/packages/pypi_repository/)_ | +| `repository-url` / `PYTHON_REPOSITORY_URL`| Target PyPI repository to publish packages | _[GitLab project's PyPI packages repository](https://docs.gitlab.com/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` | | `py-release-job-tags` / `PY_RELEASE_JOB_TAGS` | Tags to be used for selecting runners for the job | `[]` | +For information on building recommandations look (`py-package`)[/README.md#py-package-job] #### Setuptools tip @@ -479,6 +533,30 @@ current_version = "0.0.0" filename = "project-name/__init__.py" ``` +#### Manage your private repositories credentials + +When using Poetry, UV or other dependency management systems, it is possible to use private repositories (for retrieving project dependencies and/or publishing your project packages). + +Each tool has its own way of providing credentials as environment variables (see [Poetry documentation](https://python-poetry.org/docs/configuration#http-basicnameusernamepassword), [UV documentation](https://docs.astral.sh/uv/configuration/environment/#uv_index_name_password)). +You should manage those credentials as any other secret (i.e. defined as [project or group CI/CD variables](https://docs.gitlab.com/ci/variables/#for-a-project), if possible [**masked**](https://docs.gitlab.com/ci/variables/#mask-a-cicd-variable) to prevent them from being inadvertently displayed in your job logs). + +##### How to propagate AWS CodeArtifact credentials + +If you're using the **AWS CodeArtifact variant**, you can propagate AWS CodeArtifact credentials (obtained dynamically by the variant) for Poetry and UV by declaring the following variables (replace the `AWS_REPO_NAME` part with your actual configured private repository name): + +```yaml +variables: + # if using Poetry + POETRY_HTTP_BASIC_AWS_REPO_NAME_USER: $PYTHON_REPOSITORY_USER + POETRY_HTTP_BASIC_AWS_REPO_NAME_PASSWORD: $PYTHON_REPOSITORY_PASSWORD + # if using UV + UV_INDEX_AWS_REPO_NAME_USERNAME: $PYTHON_REPOSITORY_USER + UV_INDEX_AWS_REPO_NAME_PASSWORD: $PYTHON_REPOSITORY_PASSWORD +``` + +:information_source: the AWS CodeArtifact variant obtains temporary authentication credentials and makes them available in the `$PYTHON_REPOSITORY_USER` / `$PYTHON_REPOSITORY_PASSWORD` variable. +The above piece of code will simply reuse those values as Poetry/UV configuration. + #### `semantic-release` integration 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. @@ -496,9 +574,9 @@ You can either use a SSH key or user/password credentials. ##### Using a SSH key -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. +We recommend you to use a [project deploy key](https://docs.gitlab.com/user/project/deploy_keys/#project-deploy-keys) with write access to your project. -The key should not have a passphrase (see [how to generate a new SSH key pair](https://docs.gitlab.com/ee/user/ssh.html#generate-an-ssh-key-pair)). +The key should not have a passphrase (see [how to generate a new SSH key pair](https://docs.gitlab.com/user/ssh/#generate-an-ssh-key-pair)). Specify :lock: `$GIT_PRIVATE_KEY` as secret project variable with the private part of the deploy key. @@ -514,11 +592,11 @@ The template handles both classic variable and file variable. Simply specify :lock: `$GIT_USERNAME` and :lock: `$GIT_PASSWORD` as secret project variables. -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 `write_repository` scope and `Maintainer` role. +Note that the password should be an access token (preferably a [project](https://docs.gitlab.com/user/project/settings/project_access_tokens/) or [group](https://docs.gitlab.com/user/group/settings/group_access_tokens/) access token) with `write_repository` scope and `Maintainer` role. #### Pip repositories -When depending on Python packages published in [GitLab's packages registry](https://docs.gitlab.com/ee/user/packages/pypi_repository/), it could be useful to configure a group level Package. +When depending on Python packages published in [GitLab's packages registry](https://docs.gitlab.com/user/packages/pypi_repository/), it could be useful to configure a group level Package. But such repository will require an authenticated access. To do so, simply set the `PIP_INDEX_URL` and use the CI job token. @@ -558,7 +636,7 @@ In order to be able to communicate with the Vault server, the variant requires t | :lock: `VAULT_ROLE_ID` | The [AppRole](https://www.vaultproject.io/docs/auth/approle) RoleID | _none_ | | :lock: `VAULT_SECRET_ID` | The [AppRole](https://www.vaultproject.io/docs/auth/approle) SecretID | _none_ | -By default, the variant will authentifacte using a [JWT ID token](https://docs.gitlab.com/ee/ci/secrets/id_token_authentication.html). To use [AppRole](https://www.vaultproject.io/docs/auth/approle) instead the `VAULT_ROLE_ID` and `VAULT_SECRET_ID` should be defined as secret project variables. +By default, the variant will authentifacte using a [JWT ID token](https://docs.gitlab.com/ci/secrets/id_token_authentication/). To use [AppRole](https://www.vaultproject.io/docs/auth/approle) instead the `VAULT_ROLE_ID` and `VAULT_SECRET_ID` should be defined as secret project variables. #### Usage @@ -580,9 +658,9 @@ With: ```yaml include: # main component - - component: $CI_SERVER_FQDN/to-be-continuous/python/gitlab-ci-python@7.8.0 + - component: $CI_SERVER_FQDN/to-be-continuous/python/gitlab-ci-python@7.10.2 # Vault variant - - component: $CI_SERVER_FQDN/to-be-continuous/python/gitlab-ci-python-vault@7.8.0 + - component: $CI_SERVER_FQDN/to-be-continuous/python/gitlab-ci-python-vault@7.10.2 inputs: vault-base-url: "https://vault.acme.host/v1" # audience claim for JWT @@ -613,21 +691,21 @@ The variant requires the additional configuration parameters: | Input / Variable | Description | Default value | | ----------------- | -------------------------------------- | ----------------- | -| `gcp-oidc-aud` / `GCP_OIDC_AUD` | The `aud` claim for the JWT token _(only required for [OIDC authentication](https://docs.gitlab.com/ee/ci/cloud_services/google_cloud/))_ | `$CI_SERVER_URL` | -| `gcp-oidc-provider` / `GCP_OIDC_PROVIDER` | Default Workload Identity Provider associated with GitLab to [authenticate with OpenID Connect](https://docs.gitlab.com/ee/ci/cloud_services/google_cloud/) | _none_ | +| `gcp-oidc-aud` / `GCP_OIDC_AUD` | The `aud` claim for the JWT token _(only required for [OIDC authentication](https://docs.gitlab.com/ci/cloud_services/google_cloud/))_ | `$CI_SERVER_URL` | +| `gcp-oidc-provider` / `GCP_OIDC_PROVIDER` | Default Workload Identity Provider associated with GitLab to [authenticate with OpenID Connect](https://docs.gitlab.com/ci/cloud_services/google_cloud/) | _none_ | | `gcp-oidc-account` / `GCP_OIDC_ACCOUNT` | Default Service Account to which impersonate with OpenID Connect authentication | _none_ | #### Example ```yaml include: - - component: $CI_SERVER_FQDN/to-be-continuous/python/gitlab-ci-python@7.8.0 + - component: $CI_SERVER_FQDN/to-be-continuous/python/gitlab-ci-python@7.10.2 # 2: set/override component inputs inputs: image: registry.hub.docker.com/library/python:3.12-slim pytest-enabled: true - - component: $CI_SERVER_FQDN/to-be-continuous/python/gitlab-ci-python-gcp@7.8.0 + - component: $CI_SERVER_FQDN/to-be-continuous/python/gitlab-ci-python-gcp@7.10.2 inputs: # common OIDC config for non-prod envs gcp-oidc-provider: "projects/<gcp_nonprod_proj_id>/locations/global/workloadIdentityPools/<pool_id>/providers/<provider_id>" @@ -648,7 +726,7 @@ Most importantly, the variant sets the `pip global.index-url` to the CodeArtifac The variant supports two authentication methods: -1. [federated authentication using OpenID Connect](https://docs.gitlab.com/ee/ci/cloud_services/aws/) (**recommended method**), +1. [federated authentication using OpenID Connect](https://docs.gitlab.com/ci/cloud_services/aws/) (**recommended method**), 2. or basic authentication with AWS access key ID & secret access key. :warning: when using this variant, you must have created the CodeArtifact repository. @@ -667,7 +745,7 @@ The variant *requires* the additional configuration parameters: ##### OIDC authentication config -This is the recommended authentication method. In order to use it, first carefuly follow [GitLab's documentation](https://docs.gitlab.com/ee/ci/cloud_services/aws/), +This is the recommended authentication method. In order to use it, first carefuly follow [GitLab's documentation](https://docs.gitlab.com/ci/cloud_services/aws/), then set the required configuration. | Input / Variable | Description | Default value | @@ -687,13 +765,13 @@ then set the required configuration. ```yaml include: - - component: $CI_SERVER_FQDN/to-be-continuous/python/gitlab-ci-python@7.8.0 + - component: $CI_SERVER_FQDN/to-be-continuous/python/gitlab-ci-python@7.10.2 # 2: set/override component inputs inputs: image: registry.hub.docker.com/library/python:3.12-slim pytest-enabled: true - - component: $CI_SERVER_FQDN/to-be-continuous/python/gitlab-ci-python-aws-codeartifact@7.8.0 + - component: $CI_SERVER_FQDN/to-be-continuous/python/gitlab-ci-python-aws-codeartifact@7.10.2 inputs: aws-region: "us-east-1" aws-codeartifact-domain: "acme" diff --git a/kicker.json b/kicker.json index 5484d7ccadc566a40c7aba8d8cc65135a47c8ec3..362ecc8cee0507ba8d630ecde4a5d144d491ecbb 100644 --- a/kicker.json +++ b/kicker.json @@ -21,7 +21,7 @@ "name": "PYTHON_BUILD_SYSTEM", "description": "Python build-system to use to install dependencies, build and package the project", "type": "enum", - "values": ["auto", "setuptools", "poetry", "pipenv", "reqfile", "uv"], + "values": ["auto", "setuptools", "poetry", "pipenv", "reqfile", "uv", "hatch"], "default": "auto", "advanced": true }, @@ -86,7 +86,7 @@ { "id":"publish", "name":"publish", - "description":"This job allows publishing the built packages to a PyPI compatible repository ([GitLab packages](https://docs.gitlab.com/ee/user/packages/pypi_repository/) by default.", + "description":"This job allows publishing the built packages to a PyPI compatible repository ([GitLab packages](https://docs.gitlab.com/user/packages/pypi_repository/) by default.", "enable_with": "PYTHON_PUBLISH_ENABLED" }, { @@ -227,6 +227,14 @@ "description": "This job generates a file listing all dependencies using [syft](https://github.com/anchore/syft)", "disable_with": "PYTHON_SBOM_DISABLED", "variables": [ + { + "name": "TBC_SBOM_MODE", + "type": "enum", + "values": ["onrelease", "always"], + "description": "Controls when SBOM reports are generated (`onrelease`: only on `$INTEG_REF`, `$PROD_REF` and `$RELEASE_REF` pipelines; `always`: any pipeline)", + "advanced": true, + "default": "onrelease" + }, { "name": "PYTHON_SBOM_SYFT_URL", "description": "Url to the `tar.gz` package for `linux_amd64` of Syft to use\n\n_When unset, the latest version will be used_", @@ -241,7 +249,7 @@ { "name": "PYTHON_SBOM_OPTS", "description": "Options for syft used for SBOM analysis", - "default": "--override-default-catalogers python-package-cataloger", + "default": "--override-default-catalogers python-package-cataloger --select-catalogers -file", "advanced": true }, { @@ -308,7 +316,7 @@ { "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/)_", + "description": "Target PyPI repository to publish packages.\n\n_defaults to [GitLab project's packages repository](https://docs.gitlab.com/user/packages/pypi_repository/)_", "default": "${CI_SERVER_URL}/api/v4/projects/${CI_PROJECT_ID}/packages/pypi" }, { @@ -461,7 +469,7 @@ "variables": [ { "name": "GCP_OIDC_AUD", - "description": "The `aud` claim for the JWT token _(only required for [OIDC authentication](https://docs.gitlab.com/ee/ci/cloud_services/google_cloud/))_", + "description": "The `aud` claim for the JWT token _(only required for [OIDC authentication](https://docs.gitlab.com/ci/cloud_services/google_cloud/))_", "default": "$CI_SERVER_URL", "advanced": true }, @@ -471,7 +479,7 @@ }, { "name": "GCP_OIDC_PROVIDER", - "description": "Default Workload Identity Provider associated with GitLab to [authenticate with OpenID Connect](https://docs.gitlab.com/ee/ci/cloud_services/google_cloud/)" + "description": "Default Workload Identity Provider associated with GitLab to [authenticate with OpenID Connect](https://docs.gitlab.com/ci/cloud_services/google_cloud/)" } ] }, @@ -493,13 +501,13 @@ }, { "name": "AWS_OIDC_AUD", - "description": "The `aud` claim for the JWT token _(only required for [OIDC authentication](https://docs.gitlab.com/ee/ci/cloud_services/aws/))_", + "description": "The `aud` claim for the JWT token _(only required for [OIDC authentication](https://docs.gitlab.com/ci/cloud_services/aws/))_", "default": "$CI_SERVER_URL", "advanced": true }, { "name": "AWS_OIDC_ROLE_ARN", - "description": "Default IAM Role ARN associated with GitLab _(only required for [OIDC authentication](https://docs.gitlab.com/ee/ci/cloud_services/aws/))_" + "description": "Default IAM Role ARN associated with GitLab _(only required for [OIDC authentication](https://docs.gitlab.com/ci/cloud_services/aws/))_" }, { "name": "AWS_ACCESS_KEY_ID", diff --git a/templates/gitlab-ci-python-aws-codeartifact.yml b/templates/gitlab-ci-python-aws-codeartifact.yml index 60baea0c05de835f801a4090f0b0db3977dc2641..81d1611dc5354452dfbe739fbca67be11e41aa56 100644 --- a/templates/gitlab-ci-python-aws-codeartifact.yml +++ b/templates/gitlab-ci-python-aws-codeartifact.yml @@ -16,11 +16,11 @@ spec: description: Default region (where the Codeartifact registry is located) default: '' aws-oidc-aud: - description: The `aud` claim for the JWT token _(only required for [OIDC authentication](https://docs.gitlab.com/ee/ci/cloud_services/aws/))_ + description: The `aud` claim for the JWT token _(only required for [OIDC authentication](https://docs.gitlab.com/ci/cloud_services/aws/))_ default: $CI_SERVER_URL aws-oidc-role-arn: description: Default IAM Role ARN associated with GitLab _(only required for [OIDC - authentication](https://docs.gitlab.com/ee/ci/cloud_services/aws/))_ + authentication](https://docs.gitlab.com/ci/cloud_services/aws/))_ default: '' --- variables: diff --git a/templates/gitlab-ci-python-gcp.yml b/templates/gitlab-ci-python-gcp.yml index 2ac425fbd57de08ef3956a99f54ea647ad4e239b..37aec7a2f965a8357ddd4a3be7ccf44ba48932bc 100644 --- a/templates/gitlab-ci-python-gcp.yml +++ b/templates/gitlab-ci-python-gcp.yml @@ -5,13 +5,13 @@ spec: inputs: gcp-oidc-aud: - description: The `aud` claim for the JWT token _(only required for [OIDC authentication](https://docs.gitlab.com/ee/ci/cloud_services/google_cloud/)))_ + description: The `aud` claim for the JWT token _(only required for [OIDC authentication](https://docs.gitlab.com/ci/cloud_services/google_cloud/)))_ default: $CI_SERVER_URL gcp-oidc-account: description: Default Service Account to which impersonate with OpenID Connect authentication default: '' gcp-oidc-provider: - description: Default Workload Identity Provider associated with GitLab to [authenticate with OpenID Connect](https://docs.gitlab.com/ee/ci/cloud_services/google_cloud/) + description: Default Workload Identity Provider associated with GitLab to [authenticate with OpenID Connect](https://docs.gitlab.com/ci/cloud_services/google_cloud/) default: '' --- variables: @@ -19,12 +19,12 @@ variables: GCP_OIDC_ACCOUNT: $[[ inputs.gcp-oidc-account ]] GCP_OIDC_PROVIDER: $[[ inputs.gcp-oidc-provider ]] -.gcp-provider-auth: - before_script: - - set -e - - echo -e "[\\e[1;94mINFO\\e[0m] Installing GCP authentication with env GOOGLE_APPLICATION_CREDENTIALS file" - - echo $GCP_JWT > "$CI_BUILDS_DIR/.auth_token.jwt" - - |- +.python-gcp-adc: + - | + if [[ "$GCP_JWT" ]] + then + echo -e "[\\e[1;94mINFO\\e[0m] Installing GCP authentication with env GOOGLE_APPLICATION_CREDENTIALS file" + echo $GCP_JWT > "$CI_BUILDS_DIR/.auth_token.jwt" cat << EOF > "$CI_BUILDS_DIR/google_application_credentials.json" { "type": "external_account", @@ -36,24 +36,25 @@ variables: }, "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/${GCP_OIDC_ACCOUNT}:generateAccessToken" } - EOF - - export GOOGLE_APPLICATION_CREDENTIALS="$CI_BUILDS_DIR/google_application_credentials.json" - + EOF + export GOOGLE_APPLICATION_CREDENTIALS="$CI_BUILDS_DIR/google_application_credentials.json" + else + echo '[WARN] $GCP_JWT is not set: cannot setup Application Default Credentials (ADC) authentication' + fi .python-base: image: $PYTHON_IMAGE services: - name: "$TBC_TRACKING_IMAGE" - command: ["--service", "python", "7.8.0"] + command: ["--service", "python", "7.10.2"] + id_tokens: + GCP_JWT: + aud: "$GCP_OIDC_AUD" variables: GCP_JWT: $GCP_JWT before_script: - - !reference [.gcp-provider-auth, before_script] - !reference [.python-scripts] + - !reference [.python-gcp-adc] - install_ca_certs "${CUSTOM_CA_CERTS:-$DEFAULT_CA_CERTS}" - cd ${PYTHON_PROJECT_DIR} - guess_build_system - - id_tokens: - GCP_JWT: - aud: "$GCP_OIDC_AUD" diff --git a/templates/gitlab-ci-python-vault.yml b/templates/gitlab-ci-python-vault.yml index e9ec807653e98c977d562bd4247d5b8e6f43c5c7..1d49fdc6b3826b137f0c3fb4e97e3e60e1b98f21 100644 --- a/templates/gitlab-ci-python-vault.yml +++ b/templates/gitlab-ci-python-vault.yml @@ -22,7 +22,7 @@ variables: .python-base: services: - name: "$TBC_TRACKING_IMAGE" - command: ["--service", "python", "7.8.0"] + command: ["--service", "python", "7.10.2"] - name: "$TBC_VAULT_IMAGE" alias: "vault-secrets-provider" variables: diff --git a/templates/gitlab-ci-python.yml b/templates/gitlab-ci-python.yml index 0ab9be523dacc74a092e09a7dc718cbcc54639db..d26fa2b5d7835c391dd7965ce03190d808d9b9ea 100644 --- a/templates/gitlab-ci-python.yml +++ b/templates/gitlab-ci-python.yml @@ -30,6 +30,7 @@ spec: - pipenv - reqfile - uv + - hatch default: auto reqs-file: description: |- @@ -125,7 +126,7 @@ spec: default: $CI_PROJECT_PATH/$PYTHON_PROJECT_DIR sbom-opts: description: Options for syft used for SBOM analysis - default: --override-default-catalogers python-package-cataloger + default: --override-default-catalogers python-package-cataloger --select-catalogers -file release-enabled: description: Enable Release type: boolean @@ -157,7 +158,7 @@ spec: description: |- Target PyPI repository to publish packages. - _defaults to [GitLab project's packages repository](https://docs.gitlab.com/ee/user/packages/pypi_repository/)_ + _defaults to [GitLab project's packages repository](https://docs.gitlab.com/user/packages/pypi_repository/)_ default: ${CI_SERVER_URL}/api/v4/projects/${CI_PROJECT_ID}/packages/pypi black-enabled: description: Enable black @@ -294,7 +295,18 @@ workflow: # else (Ready MR): auto & failing - when: on_success +# software delivery job prototype: run on production and integration branches + release pipelines +.delivery-policy: + rules: + # on tag with release pattern + - if: '$CI_COMMIT_TAG =~ $RELEASE_REF' + # on production or integration branch(es) + - if: '$CI_COMMIT_REF_NAME =~ $PROD_REF || $CI_COMMIT_REF_NAME =~ $INTEG_REF' + variables: + # Global TBC SBOM Mode (onrelease -> only generate SBOMs for releases, always -> generate SBOMs for all refs) + TBC_SBOM_MODE: "onrelease" + # Default Docker image (can be overridden) PYTHON_IMAGE: $[[ inputs.image ]] # Default Python project root directory @@ -327,7 +339,7 @@ variables: PYTHON_RELEASE_COMMIT_MESSAGE: $[[ inputs.release-commit-message ]] # 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 + # https://docs.gitlab.com/user/packages/pypi_repository/#authenticate-with-a-ci-job-token PYTHON_REPOSITORY_URL: $[[ inputs.repository-url ]] PYTHON_REPOSITORY_USERNAME: gitlab-ci-token PYTHON_REPOSITORY_PASSWORD: $CI_JOB_TOKEN @@ -382,6 +394,17 @@ variables: echo -e "[\\e[1;91mERROR\\e[0m] $*" } + function log_elapsed_time() { + _end_time=$(get_current_ts_ms) + _named=$1 + _start_time=$2 + + _diff_ms=$((_end_time - _start_time)) + _elapsed_sec="$((_diff_ms / 1000)).$(((_diff_ms % 1000) / 100))" + + log_info " *** ${_named} took \\e[32m$_elapsed_sec\\e[0m seconds" + } + function fail() { log_error "$*" exit 1 @@ -395,6 +418,16 @@ variables: fi } + function get_current_ts_ms() { + if command -v busybox > /dev/null + then + # no easy way to retrieve ms in BusyBox + echo $(($(date +%s) * 1000)) + else + echo $(($(date +%s%N) / 1000000)) + fi + } + function install_ca_certs() { certs=$1 if [[ -z "$certs" ]] @@ -635,12 +668,26 @@ variables: fi } + function enforce_python_cmd() { + _p3=$(command -v python3) + if [[ "$_p3" ]] && ! command -v python > /dev/null + then + _p3dir=$(dirname "$_p3") + ln -s "$_p3" "$_p3dir/python" + if [ -n "$TRACE" ]; then + log_info "python3 symlinked to $_p3dir/python" + fi + fi + } function guess_build_system() { + _start_time=$(get_current_ts_ms) + case "${PYTHON_BUILD_SYSTEM:-auto}" in auto) ;; - poetry*|setuptools*|pipenv*|uv*) - log_info "--- Build system explicitly declared: ${PYTHON_BUILD_SYSTEM}" + poetry*|setuptools*|pipenv*|uv*|hatch*) + export PYTHON_BUILD_SYSTEM_CMD="${PYTHON_BUILD_SYSTEM%%[=<>]*}" + log_info "--- Build system explicitly declared: ${PYTHON_BUILD_SYSTEM} cmd=\\e[33m$PYTHON_BUILD_SYSTEM_CMD\\e[0m" return ;; reqfile) @@ -665,6 +712,7 @@ variables: then log_info "--- Build system auto-detected: uv (uv.lock and pyproject.toml)" export PYTHON_BUILD_SYSTEM="uv" + export PYTHON_BUILD_SYSTEM_CMD="uv" return fi log_error "--- Build system auto-detected: uv (uv.lock) but no pyproject.toml found: please read template doc" @@ -682,6 +730,13 @@ variables: poetry.core.masonry.api) log_info "--- Build system auto-detected: PEP 517 with Poetry backend" export PYTHON_BUILD_SYSTEM="poetry" + export PYTHON_BUILD_SYSTEM_CMD="poetry" + return + ;; + hatchling.build) + log_info "--- Build system auto-detected: PEP 517 with Hatch backend" + export PYTHON_BUILD_SYSTEM="hatch" + export PYTHON_BUILD_SYSTEM_CMD="hatch" return ;; setuptools.build_meta) @@ -708,17 +763,12 @@ variables: log_error "--- Build system auto-detect failed: please read template doc" exit 1 fi - } - function maybe_install_poetry() { - if [[ "$PYTHON_BUILD_SYSTEM" =~ ^poetry ]] && ! command -v poetry > /dev/null - then - # shellcheck disable=SC2086 - pip install ${PIP_OPTS} "$PYTHON_BUILD_SYSTEM" - fi - } - function maybe_install_uv() { - if [[ "$PYTHON_BUILD_SYSTEM" =~ ^uv ]] && ! command -v uv > /dev/null + log_elapsed_time "guess_build_system" "$_start_time" + } + + function maybe_install_build_system() { + if ! command -v "$PYTHON_BUILD_SYSTEM_CMD" > /dev/null then # shellcheck disable=SC2086 pip install ${PIP_OPTS} "$PYTHON_BUILD_SYSTEM" @@ -727,23 +777,23 @@ variables: # install requirements function install_requirements() { + _start_time=$(get_current_ts_ms) + case "$PYTHON_BUILD_SYSTEM" in poetry*) if [[ ! -f "poetry.lock" ]]; then log_warn "Using Poetry but \\e[33;1mpoetry.lock\\e[0m file not found: you shall commit it with your project files" fi - maybe_install_poetry + maybe_install_build_system poetry install ${PYTHON_EXTRA_DEPS:+--extras "$PYTHON_EXTRA_DEPS"} ;; setuptools*) - # shellcheck disable=SC2086 - pip install ${PIP_OPTS} "$PYTHON_BUILD_SYSTEM" + maybe_install_build_system # shellcheck disable=SC2086 pip install ${PIP_OPTS} ".${PYTHON_EXTRA_DEPS:+[$PYTHON_EXTRA_DEPS]}" ;; pipenv*) - # shellcheck disable=SC2086 - pip install ${PIP_OPTS} "$PYTHON_BUILD_SYSTEM" + maybe_install_build_system if [[ ! -f "Pipfile.lock" ]]; then log_warn "Using Pipenv but \\e[33;1mPipfile.lock\\e[0m file not found: you shall commit it with your project files" pipenv install --dev --system @@ -772,21 +822,31 @@ variables: if [[ ! -f "uv.lock" ]]; then log_warn "Using uv but \\e[33;1muv.lock\\e[0m file not found: you shall commit it with your project files" fi - maybe_install_uv + maybe_install_build_system uv sync --frozen ${PYTHON_EXTRA_DEPS:+--extra "$PYTHON_EXTRA_DEPS"} ;; + hatch*) + maybe_install_build_system + hatch env create + ;; esac + + log_elapsed_time "install_requirements" "$_start_time" } function _run() { if [[ "$PYTHON_BUILD_SYSTEM" =~ ^poetry ]] then - maybe_install_poetry + maybe_install_build_system poetry run "$@" elif [[ "$PYTHON_BUILD_SYSTEM" =~ ^uv ]] then - maybe_install_uv + maybe_install_build_system uv run "$@" + elif [[ "$PYTHON_BUILD_SYSTEM" =~ ^hatch ]] + then + maybe_install_build_system + $PYTHON_BUILD_SYSTEM_CMD run "$@" else "$@" fi @@ -797,31 +857,49 @@ variables: } function _pip() { + cmd=$1 + shift + if [[ "$PYTHON_BUILD_SYSTEM" =~ ^uv ]] then - maybe_install_uv + maybe_install_build_system # shellcheck disable=SC2086 - uv pip ${PIP_OPTS} "$@" + uv pip "$cmd" ${PIP_OPTS} "$@" else # shellcheck disable=SC2086 - _run pip ${PIP_OPTS} "$@" + _run pip "$cmd" ${PIP_OPTS} "$@" fi } function py_package() { + _start_time=$(get_current_ts_ms) + + # clean reports for this job but there not impact on other jobs + rm -fr "$PYTHON_PROJECT_DIR/reports" + if [[ "$PYTHON_BUILD_SYSTEM" =~ ^poetry ]] then - maybe_install_poetry - poetry build + maybe_install_build_system + log_info "--- build packages (poetry)..." + poetry build ${TRACE+--verbose} elif [[ "$PYTHON_BUILD_SYSTEM" =~ ^uv ]] then - maybe_install_uv - uv build + maybe_install_build_system + log_info "--- build packages (uv)..." + uv build ${TRACE+--verbose} + elif [[ "$PYTHON_BUILD_SYSTEM" =~ ^hatch ]] + then + log_info "--- build packages (hatch)..." + maybe_install_build_system + $PYTHON_BUILD_SYSTEM_CMD build else + log_info "--- build packages ..." # shellcheck disable=SC2086 pip install ${PIP_OPTS} build python -m build fi + + log_elapsed_time "py_package" "$_start_time" } function configure_scm_auth() { @@ -847,7 +925,31 @@ variables: fi } + function py_bump_my_version() { + mkdir -p -m 777 tbc_tmp >&2 + py_cur_version="$1" + py_release_part="$2" + echo "$py_cur_version" > tbc_tmp/version.txt + _pip install bump-my-version >&2 + log_info "[bump-my-version] increase \\e[1;94m${py_release_part}\\e[0m (from current \\e[1;94m${py_cur_version}\\e[0m)" >&2 + _run bump-my-version bump ${TRACE+--verbose} --current-version "$py_cur_version" "$py_release_part" tbc_tmp/version.txt >&2 + cat tbc_tmp/version.txt + rm -fr tbc_tmp/version.txt >&2 + } + + function py_commit_pyproject() { + py_cur_version="$1" + py_next_version="$2" + # Git commit and tag + git add pyproject.toml + # emulate bump-my-version to generate commit message + py_commit_message=$(python -c "print('$PYTHON_RELEASE_COMMIT_MESSAGE'.format(current_version='$py_cur_version', new_version='$py_next_version'))") + git commit -m "$py_commit_message" + git tag "$py_next_version" + } + function py_release() { + log_info "PYTHON_RELEASE_ENABLED:$PYTHON_RELEASE_ENABLED PYTHON_PUBLISH_ENABLED:$PYTHON_PUBLISH_ENABLED" # 1: retrieve next release info from semantic-release if [ "$SEMREL_INFO_ON" ] && [ "$PYTHON_SEMREL_RELEASE_DISABLED" != "true" ] then @@ -866,7 +968,7 @@ variables: # 2: bump-my-version (+ Git commit & tag) if [[ "$PYTHON_BUILD_SYSTEM" =~ ^poetry ]] then - maybe_install_poetry + maybe_install_build_system if [[ -z "$py_next_version" ]] then py_cur_version=$(poetry version --short) @@ -884,7 +986,7 @@ variables: git tag "$py_next_version" elif [[ "$PYTHON_BUILD_SYSTEM" =~ ^uv ]] then - maybe_install_uv + maybe_install_build_system if [[ -z "$py_next_version" ]] then # quick version waiting for uv to manage bump @@ -909,6 +1011,19 @@ variables: py_commit_message=$(python -c "print('$PYTHON_RELEASE_COMMIT_MESSAGE'.format(current_version='$py_cur_version', new_version='$py_next_version'))") git commit -m "$py_commit_message" git tag --force "$py_next_version" + elif [[ "$PYTHON_BUILD_SYSTEM" =~ ^hatch ]] + then + maybe_install_build_system + if [[ -z "$py_next_version" ]] + then + py_cur_version=$(hatch version) + py_next_version=$(py_bump_my_version "$py_cur_version" "$PYTHON_RELEASE_NEXT") + fi + + log_info "[hatch] change version \\e[1;94m${py_cur_version}\\e[0m → \\e[1;94m${py_next_version}\\e[0m" + _pip install toml-cli + _run toml set --toml-path pyproject.toml project.version "$py_next_version" + py_commit_pyproject "$py_cur_version" "$py_next_version" else # Setuptools / bump-my-version # shellcheck disable=SC2086 @@ -920,12 +1035,19 @@ variables: # create cfg in case it doesn't exist - will be updated by bumpversion if [[ ! "$py_cur_version" && ! -f ".bumpversion.cfg" && ! -f ".bumpversion.toml" && ! -f "pyproject.toml" && ! -f "setup.cfg" ]] then - log_error "Current version not defined and not version file found, set initial version at least in .bumpversion.toml or pyproject.toml" + log_warn "Current version not defined and no version file found: please set initial version in .bumpversion.toml, .bumpversion.cfg, setup.cfg or pyproject.toml ([tool.bumpversion] section)" fi bump-my-version bump ${TRACE+--verbose} --current-version "${py_cur_version:-${PYTHON_RELEASE_START_VERSION:-0.0.0}}" --new-version "$py_next_version" --commit ${PYTHON_RELEASE_COMMIT_MESSAGE:+--message "$PYTHON_RELEASE_COMMIT_MESSAGE"} --tag --tag-name "{new_version}" "$py_release_part" - elif [[ -f ".bumpversion.cfg" ]] + elif [[ -f ".bumpversion.cfg" || -f ".bumpversion.toml" || -f "pyproject.toml" || -f "setup.cfg" ]] then - # current version shall be set in .bumpversion.cfg + if [[ ! -f ".bumpversion.cfg" && ! -f ".bumpversion.toml" && ! -f "setup.cfg" ]] # pyproject.toml case + then + if ! grep -q '^[[:space:]]*\[tool\.bumpversion\]' "pyproject.toml" + then + log_warn "Current version not defined and no version file found: please set initial version in pyproject.toml ([tool.bumpversion] section)" + fi + fi + # current version shall be set in bump-my-version config file py_release_part="$PYTHON_RELEASE_NEXT" log_info "[bump-my-version bump] increase \\e[1;94m${py_release_part}\\e[0m" bump-my-version bump ${TRACE+--verbose} --commit ${PYTHON_RELEASE_COMMIT_MESSAGE:+--message "$PYTHON_RELEASE_COMMIT_MESSAGE"} --tag --tag-name "{new_version}" "$py_release_part" @@ -937,7 +1059,7 @@ variables: log_info "[bump-my-version] increase \\e[1;94m${py_release_part}\\e[0m (from current \\e[1;94m${py_cur_version}\\e[0m)" bump-my-version bump ${TRACE+--verbose} --current-version "$py_cur_version" --commit ${PYTHON_RELEASE_COMMIT_MESSAGE:+--message "$PYTHON_RELEASE_COMMIT_MESSAGE"} --tag --tag-name "{new_version}" "$py_release_part" setup.py else - log_error "--- setup.py or .bumpversion.cfg file required to retrieve current version: cannot perform release" + log_error "--- setup.py, .bumpversion.toml, pyproject.toml, .bumpversion.cfg(deprecated) or setup.cfg(deprecated) file required to retrieve current version: cannot perform release" exit 1 fi fi @@ -949,42 +1071,34 @@ variables: } function py_publish() { - if [[ "$PYTHON_BUILD_SYSTEM" =~ ^poetry ]] + if [[ "$PYTHON_PACKAGE_ENABLED" != "true" ]] then - maybe_install_poetry + py_package + fi - if [[ "$PYTHON_PACKAGE_ENABLED" != "true" ]] - then - log_info "--- build packages (poetry)..." - poetry build ${TRACE+--verbose} - fi + if [[ "$PYTHON_BUILD_SYSTEM" =~ ^poetry ]] + then + maybe_install_build_system log_info "--- publish packages (poetry) to $PYTHON_REPOSITORY_URL with user $PYTHON_REPOSITORY_USERNAME..." poetry config repositories.user_defined "$PYTHON_REPOSITORY_URL" poetry publish ${TRACE+--verbose} --username "$PYTHON_REPOSITORY_USERNAME" --password "$PYTHON_REPOSITORY_PASSWORD" --repository user_defined elif [[ "$PYTHON_BUILD_SYSTEM" =~ ^uv ]] then - maybe_install_uv + maybe_install_build_system - if [[ "$PYTHON_PACKAGE_ENABLED" != "true" ]] - then - log_info "--- build packages (uv)..." - uv build ${TRACE+--verbose} - fi - log_info "--- publish packages (uv) to $PYTHON_REPOSITORY_URL with user $PYTHON_REPOSITORY_USERNAME..." uv publish ${TRACE+--verbose} --username "$PYTHON_REPOSITORY_USERNAME" --password "$PYTHON_REPOSITORY_PASSWORD" --publish-url "$PYTHON_REPOSITORY_URL" + elif [[ "$PYTHON_BUILD_SYSTEM" =~ ^hatch ]] + then + maybe_install_build_system + + log_info "--- publish packages (hatch) to $PYTHON_REPOSITORY_URL with user $PYTHON_REPOSITORY_USERNAME..." + hatch publish ${TRACE+--verbose} --no-prompt --yes --user "$PYTHON_REPOSITORY_USERNAME" --auth "$PYTHON_REPOSITORY_PASSWORD" --repo "$PYTHON_REPOSITORY_URL" else # shellcheck disable=SC2086 pip install ${PIP_OPTS} build twine - if [[ "$PYTHON_PACKAGE_ENABLED" != "true" ]] - then - log_info "--- build packages (build)..." - rm -rf dist - python -m build - fi - log_info "--- publish packages (twine) to $PYTHON_REPOSITORY_URL with user $PYTHON_REPOSITORY_USERNAME..." twine upload ${TRACE+--verbose} --username "$PYTHON_REPOSITORY_USERNAME" --password "$PYTHON_REPOSITORY_PASSWORD" --repository-url "$PYTHON_REPOSITORY_URL" dist/* fi @@ -1029,7 +1143,7 @@ stages: image: $PYTHON_IMAGE services: - name: "$TBC_TRACKING_IMAGE" - command: ["--service", "python", "7.8.0"] + command: ["--service", "python", "7.10.2"] variables: # set local cache dir; most Python tools honour XDG specs XDG_CACHE_HOME: "$CI_PROJECT_DIR/.cache" @@ -1037,6 +1151,8 @@ stages: POETRY_CACHE_DIR: "$CI_PROJECT_DIR/.cache/poetry" PIPENV_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pipenv" UV_CACHE_DIR: "$CI_PROJECT_DIR/.cache/uv" + HATCH_CACHE_DIR: "$CI_PROJECT_DIR/.cache/hatch/.cache/" + HATCH_DATA_DIR: "$CI_PROJECT_DIR/.cache/hatch/.local" POETRY_VIRTUALENVS_IN_PROJECT: "false" cache: key: "$CI_COMMIT_REF_SLUG-python" @@ -1046,6 +1162,7 @@ stages: before_script: - !reference [.python-scripts] - install_ca_certs "${CUSTOM_CA_CERTS:-$DEFAULT_CA_CERTS}" + - enforce_python_cmd - cd ${PYTHON_PROJECT_DIR} - guess_build_system @@ -1092,7 +1209,7 @@ py-lint: - install_requirements - _pip install pylint_gitlab # codeclimate reports # run pylint and generate reports all at once - - _run pylint --output-format=colorized,pylint_gitlab.GitlabCodeClimateReporter:reports/py-lint.codeclimate.json,parseable:reports/py-lint.parseable.txt ${PYLINT_ARGS} ${PYLINT_FILES:-$(find -type f -name "*.py" -not -path "./.cache/*")} + - _run pylint --output-format=colorized,pylint_gitlab.GitlabCodeClimateReporter:reports/py-lint.codeclimate.json,parseable:reports/py-lint.parseable.txt ${PYLINT_ARGS} ${PYLINT_FILES:-$(find -type f -name "*.py" -not -path "./.cache/*" -not -path "./.venv/*")} artifacts: name: "$CI_JOB_NAME artifacts from $CI_PROJECT_NAME on $CI_COMMIT_REF_SLUG" expire_in: 1 day @@ -1255,7 +1372,7 @@ py-pytest: - mkdir -p -m 777 reports - install_requirements - _pip install pytest pytest-cov coverage - - _python -m pytest --junit-xml=reports/TEST-pytests.xml --cov --cov-report term --cov-report xml:reports/py-coverage.cobertura.xml ${PYTEST_ARGS} + - _run pytest --junit-xml=reports/TEST-pytests.xml --cov --cov-report term --cov-report xml:reports/py-coverage.cobertura.xml ${PYTEST_ARGS} rules: # skip if $PYTEST_ENABLED not set - if: '$PYTEST_ENABLED != "true"' @@ -1348,9 +1465,14 @@ py-trivy: ;; uv*) log_info "$PYTHON_BUILD_SYSTEM build system used (\\e[32mmust generate pinned requirements.txt from uv.lock\\e[0m)" - maybe_install_uv + maybe_install_build_system uv export > ./reports/requirements.txt ;; + hatch*) + log_info "$PYTHON_BUILD_SYSTEM build system used (\\e[32mmust generate pinned requirements.txt\\e[0m)" + maybe_install_build_system + hatch run python -m pip freeze > reports/requirements.txt # hatch dep show requirements not working and complete + ;; *) log_info "$PYTHON_BUILD_SYSTEM build system used (\\e[32mmust generate pinned requirements.txt\\e[0m)" install_requirements @@ -1408,9 +1530,14 @@ py-sbom: ;; uv*) log_info "$PYTHON_BUILD_SYSTEM build system used (\\e[32mmust generate pinned requirements.txt from uv.lock\\e[0m)" - maybe_install_uv + maybe_install_build_system uv export > ./reports/requirements.txt ;; + hatch*) + log_info "$PYTHON_BUILD_SYSTEM build system used (\\e[32mmust generate pinned requirements.txt from uv.lock\\e[0m)" + maybe_install_build_system + hatch run python -m pip freeze > reports/requirements.txt # hatch dep show requirements not working and complete + ;; *) log_info "$PYTHON_BUILD_SYSTEM build system used (\\e[32mmust generate pinned requirements.txt\\e[0m)" install_requirements @@ -1447,7 +1574,13 @@ py-sbom: # exclude if disabled - if: '$PYTHON_SBOM_DISABLED == "true"' when: never - - !reference [.test-policy, rules] + # 'always' mode: run + - if: '$TBC_SBOM_MODE == "always"' + # exclude unsupported modes + - if: '$TBC_SBOM_MODE != "onrelease"' + when: never + # 'onrelease' mode: use common software delivery rules + - !reference [.delivery-policy, rules] tags: $[[ inputs.py-sbom-job-tags ]] # (manual from master branch): triggers a release (tag creation)