# GitLab CI template for semantic-release

This project implements a GitLab CI/CD template to automate your versioning and release management with [semantic-release](https://github.com/semantic-release/semantic-release), supporting one or several of the following features:

* determine the next release version number,
* generate the changelog,
* commit any changed resource to the Git repository,
* create and push the Git tag,
* publish the packages (in [GitLab](https://docs.gitlab.com/ee/user/project/releases/index.html) or any other package repository of your choice),
* any additional custom behavior you are able to script, triggered on the [release steps](https://semantic-release.gitbook.io/semantic-release/#release-steps).

## Usage

This template can be used both as a [CI/CD component](https://docs.gitlab.com/ee/ci/components/#use-a-component-in-a-cicd-configuration) or using the legacy [`include:project`](https://docs.gitlab.com/ee/ci/yaml/index.html#includeproject) syntax.

### Use as a CI/CD component

Add the following to your `gitlab-ci.yml`:

```yaml
include:
  # 1: include the component
  - component: gitlab.com/to-be-continuous/semantic-release/gitlab-ci-semrel@3.11.0
    # 2: set/override component inputs
    inputs:
      changelog-enabled: true # ⚠ this is only an example
```

### Use as a CI/CD template (legacy)

Add the following to your `gitlab-ci.yml`:

```yaml
include:
  # 1: include the template
  - project: 'to-be-continuous/semantic-release'
    ref: '3.11.0'
    file: '/templates/gitlab-ci-semrel.yml'

variables:
  # 2: set/override template variables
  SEMREL_CHANGELOG_ENABLED: "true" # ⚠ this is only an example
```

## Global configuration

The semantic-release template uses some global configuration used throughout all jobs.

| Input / Variable                                         | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          | Default value                                 |
| -------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------- |
| `image` / `SEMREL_IMAGE`                                 | The Docker image used to run semantic-release                                                                                                                                                                                                                                                                                                                                                                                                                                                        | `registry.hub.docker.com/library/node:lts-slim` |
| `version` / `SEMREL_VERSION`                             | The [semantic-release](https://www.npmjs.com/package/semantic-release) version to use                                                                                                                                                                                                                                                                                                                                                                                                                | `latest`                                      |
| `exec-version` / `SEMREL_EXEC_VERSION`                   | The [@semantic-release/exec](https://www.npmjs.com/package/@semantic-release/exec) version to use                                                                                                                                                                                                                                                                                                                                                                                                    | `latest`                                      |
| :lock: `GITLAB_TOKEN`                                    | A GitLab [project access token](https://docs.gitlab.com/ee/user/project/settings/project_access_tokens.html) or [personal access token](https://docs.gitlab.com/ce/user/profile/personal_access_tokens.html) with `api`, `read_repository` and `write repository` scopes. :warning: This variable is **mandatory** and [defined by `semantic-release`](https://github.com/semantic-release/semantic-release/blob/master/docs/usage/ci-configuration.md#push-access-to-the-remote-repository) itself. | _none_                                        |
| :lock: `GIT_AUTHOR_EMAIL`                                | A Git author email address associated with the `GITLAB_TOKEN` [bot user](https://docs.gitlab.com/ee/user/project/settings/project_access_tokens.html#bot-users-for-projects). This is [defined by `semantic-release`](https://semantic-release.gitbook.io/semantic-release/usage/configuration#git-environment-variables) itself, and **required if** the [verify-user push rules](https://docs.gitlab.com/ee/user/project/repository/push_rules.html#verify-users) enabled for the project          | _none_                                        |
| :lock: `GIT_COMMITTER_EMAIL`                             | A Git committer email address associated with the `GITLAB_TOKEN` [bot user](https://docs.gitlab.com/ee/user/project/settings/project_access_tokens.html#bot-users-for-projects). This is [defined by `semantic-release`](https://semantic-release.gitbook.io/semantic-release/usage/configuration#git-environment-variables) itself, and **required if** the [verify-user push rules](https://docs.gitlab.com/ee/user/project/repository/push_rules.html#verify-users) enabled for the project       | _none_                                        |
| `config-dir` / `SEMREL_CONFIG_DIR`                       | directory containing your [semantic-release configuration](https://semantic-release.gitbook.io/semantic-release/usage/configuration#configuration-file)                                                                                                                                                                                                                                                                                                                                              | `.`                                           |
| `required-plugins-file` / `SEMREL_REQUIRED_PLUGINS_FILE` | An optional file for additional npm packages to install                                                                                                                                                                                                                                                                                                                                                                                                                                              | `semrel-required-plugins.txt`                 |

Jobs will extract required plugin packages from discovered configuration. If your configuration needs additional packages, add them, one per line, to `SEMREL_REQUIRED_PLUGINS_FILE` file. Each line must be a valid `npm install` package argument.

## Jobs

### `semantic-release` job

This job runs `semantic-release` in `ci` mode.

:warning: This template supports all [semantic-release configuration files](https://semantic-release.gitbook.io/semantic-release/usage/configuration#configuration-file) __except for__ `release.config.js` and custom CLI arguments.

If no configuration is found, the template will generate one with the following options:

* `debug`: `true` if the `$TRACE` variable is set, `false` otherwise
* `dryRun`: `true` if the `$SEMREL_DRY_RUN` variable is set, `false` otherwise
* `tagFormat`: see `$SEMREL_TAG_FORMAT` variable
* `plugins`:
    * [@semantic-release/commit-analyzer](https://github.com/semantic-release/commit-analyzer)
    * [@semantic-release/release-notes-generator](https://github.com/semantic-release/release-notes-generator)
    * [@semantic-release/gitlab](https://github.com/semantic-release/gitlab)
    * [@semantic-release/git](https://github.com/semantic-release/git)
    * optional [@semantic-release/changelog](https://github.com/semantic-release/changelog) if `SEMREL_CHANGELOG_ENABLED` is set to `true`
    * optional [@semantic-release/exec](https://github.com/semantic-release/exec) if any hook script is found (see [hook scripts](#hook_scripts))
* `branches`: `master`

#### Variables

As specified in the previous chapter, these variables are only used to generated a `.releaserc` when no configuration is found in the repository.

| Input / Variable                                       | Description                                                                                                                                                                                                                         | Default value                                                                                                                         |
| ------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `changelog-enabled` / `SEMREL_CHANGELOG_ENABLED`       | Add the [@semantic-release/changelog](https://github.com/semantic-release/changelog) plugin which will commit a changelog file in the repository if set to `true`.                                                                  | _none_                                                                                                                                |
| `changelog-file` / `SEMREL_CHANGELOG_FILE`             | [changelogFile @semantic-release/changelog option](https://github.com/semantic-release/changelog#options).                                                                                                                          | _none_ (use the plugin default value which is `CHANGELOG.md`).                                                                        |
| `changelog-title` / `SEMREL_CHANGELOG_TITLE`           | [changelogTitle @semantic-release/changelog option](https://github.com/semantic-release/changelog#options). You might want to use markdown format (for example `# MyApp Changelog`).                                                | _none_                                                                                                                                |
| `dry-run` / `SEMREL_DRY_RUN`                           | Activate the [dryRun semantic-release option](https://github.com/semantic-release/semantic-release/blob/master/docs/usage/configuration.md#dryrun) if present.                                                                      | _none_                                                                                                                                |
| `auto-release-enabled` / `SEMREL_AUTO_RELEASE_ENABLED` | When set to `true` the job start automatically. When not set (default), the job is manual.                                                                                                                                          | _none_                                                                                                                                |
| `branches-ref` / `SEMREL_BRANCHES_REF`                 | Regular expression pattern matching branches from which releases should happen (should match your [semantic-release configuration](https://semantic-release.gitbook.io/semantic-release/usage/configuration#branches))              | `/^(master|main)$/` |
| `tag-format` / `SEMREL_TAG_FORMAT`                     | [tagFormat semantic-release option](https://github.com/semantic-release/semantic-release/blob/master/docs/usage/configuration.md#tagformat). :warning: don't forget to double the `$` character so it is not interpreted by GitLab. | `$${version}`                                                                                                                         |
| `hooks-dir` / `SEMREL_HOOKS_DIR`                       | [Hook scripts](#hook_scripts) folder.                                                                                                                                                                                               | `.`                                                                                                                                   |
| `commit-message` / `SEMREL_COMMIT_MESSAGE`             | Add a custom commit message based on [semantic-release/git option](https://github.com/semantic-release/git#message).                                                                                                                | _none_ (uses semantic-release default commit message)                                                                                 |
| `release-disabled` / `SEMREL_RELEASE_DISABLED`         | Disable this job.                                                                                                                                                                                                                   | _none_                                                                                                                                |

#### Hook scripts

The generated `.releaserc` will include the [@semantic-release/exec](https://github.com/semantic-release/exec) plugin if any of the following scripts is found in the `$SEMREL_HOOKS_DIR` folder:

##### verify-conditions.sh

See [exec verifyConditionsCmd](https://github.com/semantic-release/exec#verifyconditionscmd).

Parameters: _none_

##### verify-release.sh

See [exec verifyReleaseCmd](https://github.com/semantic-release/exec#verifyreleasecmd).

Parameters:

1. Last release version
2. next release version
3. next release type

##### prepare.sh

See [exec prepareCmd](https://github.com/semantic-release/exec#preparecmd).

Parameters:

1. Last release version
2. next release version
3. next release type

##### publish.sh

See [exec publishCmd](https://github.com/semantic-release/exec#publishcmd).

Parameters:

1. Last release version
2. next release version
3. release branch
4. commits count
5. current date

##### success.sh

See [exec successCmd](https://github.com/semantic-release/exec#successcmd).

Parameters:

1. Last release version
2. next release version

##### fail.sh

See [exec failcmd](https://github.com/semantic-release/exec#failcmd).

Parameters:

1. Last release version
2. next release version

#### Signing release commits with GPG

For an introduction on commit signing, see [GitLab documentation](https://docs.gitlab.com/ee/user/project/repository/gpg_signed_commits/).

To make semantic-release sign its commits, use the following variable.

| Input / Variable            | Description                                                                                                                                                                                              | Default value |
| --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- |
| :lock: `SEMREL_GPG_SIGNKEY` | Path to the GPG signkey exported with `gpg --armor --export-secret-key`<br/>:warning: Declare as a masked [project variable of File type](https://docs.gitlab.com/ee/ci/variables/#cicd-variable-types). | _none_        |

### `semantic-release-info` job

This job (disabled by default) runs `semantic-release` with `dry-run` mode in `.pre` stage to save the following variables as [dotenv artifact](https://docs.gitlab.com/ee/ci/pipelines/job_artifacts.html#artifactsreportsdotenv) making them available for the next pipeline stages:

* `SEMREL_INFO_LAST_VERSION`: latest released version
* `SEMREL_INFO_NEXT_VERSION`: next release version (based on actual commits and comments)
* `SEMREL_INFO_NEXT_VERSION_TYPE`: next release type (`major`|`minor`|`patch`)

:warning: `SEMREL_INFO_NEXT_VERSION` and `SEMREL_INFO_NEXT_VERSION_TYPE` **wont** be available when semantic-release commits analysis determine that no release will be performed.

This job can be enabled by defining the `SEMREL_INFO_ON` variable:

* `prod` to enable on production branch only (`main` or `master` by default with `PROD_REF` environment variable)
* `branches-ref` to enable on branches associated with `branches-ref` component configuration or `SEMREL_BRANCHES_REF` environment variable (`main` or `master` by default as it fallbacks on `PROD_REF` environment variable).
* `protected` to enable on protected references
* `all` to enable on all Git references. :warning: Beware that this job requires the `GITLAB_TOKEN` variable so you must unprotect it (this will make privilege escalation possible from developer to maintainer).

#### Semantic Release Commit Analyzer and Release Notes Configuration

Semantic Release determines the semantic version, major.minor.patch, with the use of `@semantic-release/commit-analyzer` and `@semantic-release/release-notes-generator` `presets`. The [**default** preset is `angular`](https://github.com/semantic-release/semantic-release?tab=readme-ov-file#commit-message-format).
The default _may_ lead to unexpected versioning or release notes, especially when not in an Angular project nor using the Angular standard.

The commit message parser may be changed by defining the `commit-spec` / `SEMREL_COMMIT_SPEC` variable:

| Input / Variable                            | Description | Default |
| ------------------------------------------- | ----------- | ------- |
| `commit-spec` / `SEMREL_COMMIT_SPEC` | commit specification `preset` (possible values: `angular`, `codemirror`, `ember`, `eslint`, `express`, `jquery`, `jshint`, `conventionalcommits` (`cc` short form supported)) | `angular` |

**Conventional Commit Specification**

The `preset` of `conventionalcommits` (or `cc`) is a good option for most users. The [specification is well defined and documented](https://www.conventionalcommits.org/en/v1.0.0/) and compatible with tools like [Husky](https://typicode.github.io/husky/) and [commitlint](https://commitlint.js.org/). Semantic Release has plans to make `conventionalcommits` the default in the future.

**Commit Message Controls**

The `commit-spec` / `SEMREL_COMMIT_SPEC` value installs the parser requirement for Semantic Release only. Adherence to a specification with commit message controls is not provided. Angular and Conventional Commits are widely supported by commitlint and [commitizen](https://github.com/commitizen), though additional `devDependencies` and configuration files may be required, please review the tooling documentation for more information. 

**Note on supporting Semantic Release versions**

If the version of Semantic Release is pinned using [`SEMREL_VERSION`](#global-configuration) prior to v24, automated versioning via commit messaging may fail in unexpected ways. See [conventional-changelog-conventionalcommits v8.0.0 breaks semantic release](https://github.com/semantic-release/release-notes-generator/issues/633) or consider upgrading the pinned version to v24 or better to restore behaviors.

## Secrets management

Here are some advices about your **secrets** (variables marked with a :lock:):

1. Manage them as [project or group CI/CD variables](https://docs.gitlab.com/ee/ci/variables/#add-a-cicd-variable-to-a-project):
    * [**masked**](https://docs.gitlab.com/ee/ci/variables/#mask-a-cicd-variable) to prevent them from being inadvertently
      displayed in your job logs,
    * [**protected**](https://docs.gitlab.com/ee/ci/variables/#protected-cicd-variables) if you want to secure some secrets
      you don't want everyone in the project to have access to (for instance production secrets).
2. In case a secret contains [characters that prevent it from being masked](https://docs.gitlab.com/ee/ci/variables/#mask-a-cicd-variable),
  simply define its value as the [Base64](https://en.wikipedia.org/wiki/Base64) encoded value prefixed with `@b64@`:
  it will then be possible to mask it and the template will automatically decode it prior to using it.
3. Don't forget to escape special characters (ex: `$` -> `$$`).
4. You can also manage secrets using Vault variant

## Using semantic-release with other to-be-continuous templates

The semantic-release template has been designed to interoperate gracefully with release-capable to-be-continuous templates.

Unfortunaltely, there isn't one single configuration that fits all needs.
Instead, the semantic-release template configuration will have to be adapted to your case.

There are actually 2 questions that will determine the required configuration:

1. which [Delivery Mode](https://to-be-continuous.gitlab.io/doc/understand/#delivery-modes) are you using in your project?
    * _Application Deployment_ mode should trigger the release directly from your production branch (`main` or `master` by default),
    * _Software Distribution_ mode should trigger the release through a tag pipeline.
2. does the release involve **changing files in your repository** (and therefore creating a Git commit)?
    * that will be the case if you use plugins such as [@semantic-release/changelog](https://github.com/semantic-release/changelog) or [semantic-release-replace](https://github.com/jpoehnelt/semantic-release-replace-plugin)

### Case 1: _Application Deployment_ mode

When using the _Application Deployment_ delivery mode in your project, the release should be triggered directly from the production branch (`main` or `master` by default).

In that case you'll need to:

1. enable the [semantic-release info job](#semantic-release-info-job),<br/>
   _this is the job that will provide next release information to the other template(s)_
   * by setting `info-on` input / `SEMREL_INFO_ON` variable to `prod` (or any suitable non-empty value)
2. disable the [semantic-release job](#semantic-release-job),<br/>
   _the release will be handled by other template(s) directly from the production branch_
   * by setting `release-disabled` input / `SEMREL_RELEASE_DISABLED` variable to `true`
3. make sure the other template(s) provide a semantic-release integration to perform the release from the semantic-release info job. 
    Templates supporting it:
    * Docker,
    * Helm,
    * Maven,
    * Python,
    * S2I.

### Case 2: _Software Distribution_ mode without any Git commit

This is the easiest case as nothing specific has to be done to address it:

* by default the semantic-release will analyse each commit on your production branch, and will possibly create a Git tag (but no Git commit) if it determined a release has to be performed,
* the Git tag will trigger a tag pipeline during which every to-be-continuous template will take care of publishing its versioned package (using the Git tag as the version) to an appropriate packages repository.

### Case 3: _Software Distribution_ mode with a Git commit

This case will occur if you configure semantic-release to modify one or several files in your Git repository (ex: `pom.xml`, `pyproject.toml`, `CHANGELOG.md`, `README.md`...)

In that case, when semantic-release determines a release is required, it will:

* modify the files,
* create a Git commit with the changes,
* create a Git tag with the next release version,
* push the commit + the tag.

Problem: by default, semantic-release creates a Git commit with comment `chore(release): ${nextRelease.version} [skip ci]`.\
:information_source: The `[skip ci]` part is problematic as it prevents GitLab from triggering the tag pipeline, therefore preventing other to-be-continuous templates from publishing their versioned packages.

To fix this, you'll have to override the default semantic-release Git commit comment in order not to prevent the tag pipeline from being triggered.
With this done:

* the semantic-release will analyse each commit on your production branch, and will possibly create a Git tag if it determined a release has to be performed,
* the Git tag will trigger a tag pipeline during which every to-be-continuous template will take care of publishing its versioned package (using the Git tag as the version) to an appropriate packages repository.

#### How to override the Git commit comment

In most cases it is recommended to use `chore(release): ${nextRelease.version} [skip ci on prod]` as message template.\
:information_source: the important part is `[skip ci on prod]` that prevents GitLab from triggering the pipeline on your production branch only, **but not the tag pipeline**.

#### Using a configuration file

If you're configuring semantic-release with a configuration file in your repository, then the Git commit message has to be configured in the [@semantic-release/git](https://github.com/semantic-release/git#message) plugin section.

Here is a `.releaserc.yaml` configuration example that auto-generates the changelog file, and also replaces the project version in the `pyproject.toml` using the [semantic-release-replace](https://github.com/jpoehnelt/semantic-release-replace-plugin) plugin:

```yaml
plugins:
  # GitLab support
  - '@semantic-release/gitlab'
  # analyses the Git commits
  - '@semantic-release/commit-analyzer'
  # generates the release note from the Git commit messages
  - '@semantic-release/release-notes-generator'
  # generates the CHANGELOG.md file
  - '@semantic-release/changelog'
  # emulates bumpversion (replaces 'version' in pyproject.toml)
  - - semantic-release-replace-plugin
    - replacements:
        - files:
            - pyproject.toml
          from:
            - ^version *= *"\d+\.\d+\.\d+"
          to: 'version = "${nextRelease.version}"'
          countMatches: true
  # git commit/push modified files
  - - '@semantic-release/git'
    - assets:
        - 'CHANGELOG.md'
        - 'pyproject.toml'
      # the commit MUST trigger a pipeline on tag (to perform publish jobs)
      # can be skipped on prod branch
      message: 'chore(release): ${nextRelease.version} [skip ci on prod]'
branches:
  - main
  - master
tagFormat: '${version}'
```

#### Using implicit configuration

If you're not configuring semantic-release with a configuration file (but using implicit configuration provided by the template), then the Git commit message can be configured with the `commit-message` input / `SEMREL_COMMIT_MESSAGE` variable:

```yaml
variables:
  # the '$' has to be doubled to prevent GitLab from expanding it as a variable
  SEMREL_COMMIT_MESSAGE: 'chore(release): $${nextRelease.version} [skip ci on prod]'
```

## Variants

The semantic-release template can be used in conjunction with template variants to cover specific cases.

### Vault variant

This variant allows delegating your secrets management to a [Vault](https://www.vaultproject.io/) server.

#### Configuration

In order to be able to communicate with the Vault server, the variant requires the additional configuration parameters:

| Input / Variable                    | Description                                                                                                                     | Default value                                                              |
| ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
| `TBC_VAULT_IMAGE`                   | The [Vault Secrets Provider](https://gitlab.com/to-be-continuous/tools/vault-secrets-provider) image to use (can be overridden) | `registry.gitlab.com/to-be-continuous/tools/vault-secrets-provider:latest` |
| `vault-base-url` / `VAULT_BASE_URL` | The Vault server base API url                                                                                                   | _none_                                                                     |
| `vault-oidc-aud` / `VAULT_OIDC_AUD` | The `aud` claim for the JWT                                                                                                     | `$CI_SERVER_URL`                                                           |
| :lock: `VAULT_ROLE_ID`              | The [AppRole](https://www.vaultproject.io/docs/auth/approle) RoleID                                                             | **must be defined**                                                        |
| :lock: `VAULT_SECRET_ID`            | The [AppRole](https://www.vaultproject.io/docs/auth/approle) SecretID                                                           | **must be defined**                                                        |

#### Usage

Then you may retrieve any of your secret(s) from Vault using the following syntax:

```text
@url@http://vault-secrets-provider/api/secrets/{secret_path}?field={field}
```

With:

| Parameter                        | Description                                                           |
| -------------------------------- | --------------------------------------------------------------------- |
| `secret_path` (_path parameter_) | this is your secret location in the Vault server                      |
| `field` (_query parameter_)      | parameter to access a single basic field from the secret JSON payload |

#### Example

```yaml
include:
  # main template
  - component: gitlab.com/to-be-continuous/semantic-release/gitlab-ci-semrel@3.11.0
  # Vault variant
  - component: gitlab.com/to-be-continuous/semantic-release/gitlab-ci-semrel-vault@3.11.0
    inputs:
      vault-base-url: "https://vault.acme.host/v1"
      # audience claim for JWT
      vault-oidc-aud: "https://vault.acme.host"

variables:
  # Secrets managed by Vault
  GITLAB_TOKEN: "@url@http://vault-secrets-provider/api/secrets/b7ecb6ebabc231/semantic-release/token?field=group-access-token"
  # $VAULT_ROLE_ID and $VAULT_SECRET_ID defined as a secret CI/CD variable
```