feat(release)!: support gitlab releases (#30802)
This commit is contained in:
parent
d5a1918eb6
commit
9dcab79b10
@ -1308,6 +1308,14 @@
|
|||||||
"children": [],
|
"children": [],
|
||||||
"disableCollapsible": false
|
"disableCollapsible": false
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "Automate GitLab Releases",
|
||||||
|
"path": "/recipes/nx-release/automate-gitlab-releases",
|
||||||
|
"id": "automate-gitlab-releases",
|
||||||
|
"isExternal": false,
|
||||||
|
"children": [],
|
||||||
|
"disableCollapsible": false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "Publish Rust Crates",
|
"name": "Publish Rust Crates",
|
||||||
"path": "/recipes/nx-release/publish-rust-crates",
|
"path": "/recipes/nx-release/publish-rust-crates",
|
||||||
@ -2554,6 +2562,14 @@
|
|||||||
"children": [],
|
"children": [],
|
||||||
"disableCollapsible": false
|
"disableCollapsible": false
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "Automate GitLab Releases",
|
||||||
|
"path": "/recipes/nx-release/automate-gitlab-releases",
|
||||||
|
"id": "automate-gitlab-releases",
|
||||||
|
"isExternal": false,
|
||||||
|
"children": [],
|
||||||
|
"disableCollapsible": false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "Publish Rust Crates",
|
"name": "Publish Rust Crates",
|
||||||
"path": "/recipes/nx-release/publish-rust-crates",
|
"path": "/recipes/nx-release/publish-rust-crates",
|
||||||
@ -2669,6 +2685,14 @@
|
|||||||
"children": [],
|
"children": [],
|
||||||
"disableCollapsible": false
|
"disableCollapsible": false
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "Automate GitLab Releases",
|
||||||
|
"path": "/recipes/nx-release/automate-gitlab-releases",
|
||||||
|
"id": "automate-gitlab-releases",
|
||||||
|
"isExternal": false,
|
||||||
|
"children": [],
|
||||||
|
"disableCollapsible": false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "Publish Rust Crates",
|
"name": "Publish Rust Crates",
|
||||||
"path": "/recipes/nx-release/publish-rust-crates",
|
"path": "/recipes/nx-release/publish-rust-crates",
|
||||||
|
|||||||
@ -3394,6 +3394,26 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"migrations": {
|
"migrations": {
|
||||||
|
"/nx-api/nx/migrations/release-version-config-changes": {
|
||||||
|
"description": "Updates release version config based on the breaking changes in Nx v21",
|
||||||
|
"file": "generated/packages/nx/migrations/release-version-config-changes.json",
|
||||||
|
"hidden": false,
|
||||||
|
"name": "release-version-config-changes",
|
||||||
|
"version": "21.0.0-beta.11",
|
||||||
|
"originalFilePath": "/packages/nx",
|
||||||
|
"path": "/nx-api/nx/migrations/release-version-config-changes",
|
||||||
|
"type": "migration"
|
||||||
|
},
|
||||||
|
"/nx-api/nx/migrations/release-changelog-config-changes": {
|
||||||
|
"description": "Updates release changelog config based on the breaking changes in Nx v21",
|
||||||
|
"file": "generated/packages/nx/migrations/release-changelog-config-changes.json",
|
||||||
|
"hidden": false,
|
||||||
|
"name": "release-changelog-config-changes",
|
||||||
|
"version": "21.0.0-beta.11",
|
||||||
|
"originalFilePath": "/packages/nx",
|
||||||
|
"path": "/nx-api/nx/migrations/release-changelog-config-changes",
|
||||||
|
"type": "migration"
|
||||||
|
},
|
||||||
"/nx-api/nx/migrations/remove-legacy-cache": {
|
"/nx-api/nx/migrations/remove-legacy-cache": {
|
||||||
"description": "Removes the legacy cache configuration from nx.json",
|
"description": "Removes the legacy cache configuration from nx.json",
|
||||||
"file": "generated/packages/nx/migrations/remove-legacy-cache.json",
|
"file": "generated/packages/nx/migrations/remove-legacy-cache.json",
|
||||||
@ -3414,16 +3434,6 @@
|
|||||||
"path": "/nx-api/nx/migrations/remove-custom-tasks-runner",
|
"path": "/nx-api/nx/migrations/remove-custom-tasks-runner",
|
||||||
"type": "migration"
|
"type": "migration"
|
||||||
},
|
},
|
||||||
"/nx-api/nx/migrations/release-version-config-changes": {
|
|
||||||
"description": "Updates release version config based on the breaking changes in Nx v21",
|
|
||||||
"file": "generated/packages/nx/migrations/release-version-config-changes.json",
|
|
||||||
"hidden": false,
|
|
||||||
"name": "release-version-config-changes",
|
|
||||||
"version": "21.0.0-beta.1",
|
|
||||||
"originalFilePath": "/packages/nx",
|
|
||||||
"path": "/nx-api/nx/migrations/release-version-config-changes",
|
|
||||||
"type": "migration"
|
|
||||||
},
|
|
||||||
"/nx-api/nx/migrations/use-legacy-cache": {
|
"/nx-api/nx/migrations/use-legacy-cache": {
|
||||||
"description": "Set `useLegacyCache` to true for migrating workspaces",
|
"description": "Set `useLegacyCache` to true for migrating workspaces",
|
||||||
"file": "generated/packages/nx/migrations/use-legacy-cache.json",
|
"file": "generated/packages/nx/migrations/use-legacy-cache.json",
|
||||||
|
|||||||
@ -1792,6 +1792,17 @@
|
|||||||
"path": "/recipes/nx-release/automate-github-releases",
|
"path": "/recipes/nx-release/automate-github-releases",
|
||||||
"tags": ["nx-release"]
|
"tags": ["nx-release"]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "automate-gitlab-releases",
|
||||||
|
"name": "Automate GitLab Releases",
|
||||||
|
"description": "",
|
||||||
|
"mediaImage": "",
|
||||||
|
"file": "shared/recipes/nx-release/automate-gitlab-releases",
|
||||||
|
"itemList": [],
|
||||||
|
"isExternal": false,
|
||||||
|
"path": "/recipes/nx-release/automate-gitlab-releases",
|
||||||
|
"tags": ["nx-release"]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "publish-rust-crates",
|
"id": "publish-rust-crates",
|
||||||
"name": "Publish Rust Crates",
|
"name": "Publish Rust Crates",
|
||||||
@ -3499,6 +3510,17 @@
|
|||||||
"path": "/recipes/nx-release/automate-github-releases",
|
"path": "/recipes/nx-release/automate-github-releases",
|
||||||
"tags": ["nx-release"]
|
"tags": ["nx-release"]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "automate-gitlab-releases",
|
||||||
|
"name": "Automate GitLab Releases",
|
||||||
|
"description": "",
|
||||||
|
"mediaImage": "",
|
||||||
|
"file": "shared/recipes/nx-release/automate-gitlab-releases",
|
||||||
|
"itemList": [],
|
||||||
|
"isExternal": false,
|
||||||
|
"path": "/recipes/nx-release/automate-gitlab-releases",
|
||||||
|
"tags": ["nx-release"]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "publish-rust-crates",
|
"id": "publish-rust-crates",
|
||||||
"name": "Publish Rust Crates",
|
"name": "Publish Rust Crates",
|
||||||
@ -3658,6 +3680,17 @@
|
|||||||
"path": "/recipes/nx-release/automate-github-releases",
|
"path": "/recipes/nx-release/automate-github-releases",
|
||||||
"tags": ["nx-release"]
|
"tags": ["nx-release"]
|
||||||
},
|
},
|
||||||
|
"/recipes/nx-release/automate-gitlab-releases": {
|
||||||
|
"id": "automate-gitlab-releases",
|
||||||
|
"name": "Automate GitLab Releases",
|
||||||
|
"description": "",
|
||||||
|
"mediaImage": "",
|
||||||
|
"file": "shared/recipes/nx-release/automate-gitlab-releases",
|
||||||
|
"itemList": [],
|
||||||
|
"isExternal": false,
|
||||||
|
"path": "/recipes/nx-release/automate-gitlab-releases",
|
||||||
|
"tags": ["nx-release"]
|
||||||
|
},
|
||||||
"/recipes/nx-release/publish-rust-crates": {
|
"/recipes/nx-release/publish-rust-crates": {
|
||||||
"id": "publish-rust-crates",
|
"id": "publish-rust-crates",
|
||||||
"name": "Publish Rust Crates",
|
"name": "Publish Rust Crates",
|
||||||
|
|||||||
@ -680,6 +680,13 @@
|
|||||||
"name": "Automate GitHub Releases",
|
"name": "Automate GitHub Releases",
|
||||||
"path": "/recipes/nx-release/automate-github-releases"
|
"path": "/recipes/nx-release/automate-github-releases"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "",
|
||||||
|
"file": "shared/recipes/nx-release/automate-gitlab-releases",
|
||||||
|
"id": "automate-gitlab-releases",
|
||||||
|
"name": "Automate GitLab Releases",
|
||||||
|
"path": "/recipes/nx-release/automate-gitlab-releases"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "",
|
"description": "",
|
||||||
"file": "shared/recipes/nx-release/publish-rust-crates",
|
"file": "shared/recipes/nx-release/publish-rust-crates",
|
||||||
|
|||||||
@ -3370,6 +3370,26 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"migrations": [
|
"migrations": [
|
||||||
|
{
|
||||||
|
"description": "Updates release version config based on the breaking changes in Nx v21",
|
||||||
|
"file": "generated/packages/nx/migrations/release-version-config-changes.json",
|
||||||
|
"hidden": false,
|
||||||
|
"name": "release-version-config-changes",
|
||||||
|
"version": "21.0.0-beta.11",
|
||||||
|
"originalFilePath": "/packages/nx",
|
||||||
|
"path": "nx/migrations/release-version-config-changes",
|
||||||
|
"type": "migration"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Updates release changelog config based on the breaking changes in Nx v21",
|
||||||
|
"file": "generated/packages/nx/migrations/release-changelog-config-changes.json",
|
||||||
|
"hidden": false,
|
||||||
|
"name": "release-changelog-config-changes",
|
||||||
|
"version": "21.0.0-beta.11",
|
||||||
|
"originalFilePath": "/packages/nx",
|
||||||
|
"path": "nx/migrations/release-changelog-config-changes",
|
||||||
|
"type": "migration"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "Removes the legacy cache configuration from nx.json",
|
"description": "Removes the legacy cache configuration from nx.json",
|
||||||
"file": "generated/packages/nx/migrations/remove-legacy-cache.json",
|
"file": "generated/packages/nx/migrations/remove-legacy-cache.json",
|
||||||
@ -3390,16 +3410,6 @@
|
|||||||
"path": "nx/migrations/remove-custom-tasks-runner",
|
"path": "nx/migrations/remove-custom-tasks-runner",
|
||||||
"type": "migration"
|
"type": "migration"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"description": "Updates release version config based on the breaking changes in Nx v21",
|
|
||||||
"file": "generated/packages/nx/migrations/release-version-config-changes.json",
|
|
||||||
"hidden": false,
|
|
||||||
"name": "release-version-config-changes",
|
|
||||||
"version": "21.0.0-beta.1",
|
|
||||||
"originalFilePath": "/packages/nx",
|
|
||||||
"path": "nx/migrations/release-version-config-changes",
|
|
||||||
"type": "migration"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"description": "Set `useLegacyCache` to true for migrating workspaces",
|
"description": "Set `useLegacyCache` to true for migrating workspaces",
|
||||||
"file": "generated/packages/nx/migrations/use-legacy-cache.json",
|
"file": "generated/packages/nx/migrations/use-legacy-cache.json",
|
||||||
|
|||||||
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"name": "release-changelog-config-changes",
|
||||||
|
"version": "21.0.0-beta.11",
|
||||||
|
"description": "Updates release changelog config based on the breaking changes in Nx v21",
|
||||||
|
"implementation": "/packages/nx/src/migrations/update-21-0-0/release-changelog-config-changes.ts",
|
||||||
|
"aliases": [],
|
||||||
|
"hidden": false,
|
||||||
|
"path": "/packages/nx",
|
||||||
|
"schema": null,
|
||||||
|
"type": "migration",
|
||||||
|
"examplesFile": "#### Nx Release Changelog Config Changes\n\nIn Nx v21, the `mapAuthorsToGitHubUsernames` changelog \"renderOption\" for the default changelog renderer was renamed to `applyUsernameToAuthors` to reflect the fact that it is no longer specific to GitHub. Most people were not setting this option directly, but if you were, it will be automatically migrated by this migration.\n\nThe migration will also update release groups changelog configuration, if applicable.\n\n#### Sample Code Changes\n\n{% tabs %}\n{% tab label=\"Before\" %}\n\n```json {% fileName=\"nx.json\" %}\n{\n \"release\": {\n \"changelog\": {\n \"workspaceChangelog\": {\n \"renderOptions\": {\n \"mapAuthorsToGitHubUsernames\": true\n }\n },\n \"projectChangelogs\": {\n \"renderOptions\": {\n \"mapAuthorsToGitHubUsernames\": false\n }\n }\n }\n }\n}\n```\n\n{% /tab %}\n{% tab label=\"After\" %}\n\n```json {% fileName=\"nx.json\" %}\n{\n \"release\": {\n \"changelog\": {\n \"workspaceChangelog\": {\n \"renderOptions\": {\n \"applyUsernameToAuthors\": true\n }\n },\n \"projectChangelogs\": {\n \"renderOptions\": {\n \"applyUsernameToAuthors\": false\n }\n }\n }\n }\n}\n```\n\n{% /tab %}\n{% /tabs %}\n"
|
||||||
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "release-version-config-changes",
|
"name": "release-version-config-changes",
|
||||||
"version": "21.0.0-beta.1",
|
"version": "21.0.0-beta.11",
|
||||||
"description": "Updates release version config based on the breaking changes in Nx v21",
|
"description": "Updates release version config based on the breaking changes in Nx v21",
|
||||||
"implementation": "/packages/nx/src/migrations/update-21-0-0/release-version-config-changes.ts",
|
"implementation": "/packages/nx/src/migrations/update-21-0-0/release-version-config-changes.ts",
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
|
|||||||
@ -551,6 +551,12 @@
|
|||||||
"tags": ["nx-release"],
|
"tags": ["nx-release"],
|
||||||
"file": "shared/recipes/nx-release/automate-github-releases"
|
"file": "shared/recipes/nx-release/automate-github-releases"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "Automate GitLab Releases",
|
||||||
|
"id": "automate-gitlab-releases",
|
||||||
|
"tags": ["nx-release"],
|
||||||
|
"file": "shared/recipes/nx-release/automate-gitlab-releases"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "Publish Rust Crates",
|
"name": "Publish Rust Crates",
|
||||||
"id": "publish-rust-crates",
|
"id": "publish-rust-crates",
|
||||||
|
|||||||
@ -75,7 +75,9 @@ Changelog render options can be passed as [an object](https://github.com/nrwl/nx
|
|||||||
// Whether or not the commit references (such as commit and/or PR links) should be included in the changelog.
|
// Whether or not the commit references (such as commit and/or PR links) should be included in the changelog.
|
||||||
"commitReferences": true,
|
"commitReferences": true,
|
||||||
// Whether or not to include the date in the version title. It can be set to false to disable it, or true to enable with the default of (YYYY-MM-DD).
|
// Whether or not to include the date in the version title. It can be set to false to disable it, or true to enable with the default of (YYYY-MM-DD).
|
||||||
"versionTitleDate": true
|
"versionTitleDate": true,
|
||||||
|
// Whether to apply usernames to authors in the Thank You section. Note, this option was called mapAuthorsToGitHubUsernames prior to Nx v21.
|
||||||
|
"applyUsernameToAuthors": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"workspaceChangelog": {
|
"workspaceChangelog": {
|
||||||
@ -85,7 +87,9 @@ Changelog render options can be passed as [an object](https://github.com/nrwl/nx
|
|||||||
// Whether or not the commit references (such as commit and/or PR links) should be included in the changelog.
|
// Whether or not the commit references (such as commit and/or PR links) should be included in the changelog.
|
||||||
"commitReferences": true,
|
"commitReferences": true,
|
||||||
// Whether or not to include the date in the version title. It can be set to false to disable it, or true to enable with the default of (YYYY-MM-DD).
|
// Whether or not to include the date in the version title. It can be set to false to disable it, or true to enable with the default of (YYYY-MM-DD).
|
||||||
"versionTitleDate": true
|
"versionTitleDate": true,
|
||||||
|
// Whether to apply usernames to authors in the Thank You section. Note, this option was called mapAuthorsToGitHubUsernames prior to Nx v21.
|
||||||
|
"applyUsernameToAuthors": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
103
docs/shared/recipes/nx-release/automate-gitlab-releases.md
Normal file
103
docs/shared/recipes/nx-release/automate-gitlab-releases.md
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
---
|
||||||
|
title: Automate GitLab Releases
|
||||||
|
description: Learn how to configure Nx Release to automatically create GitLab releases with changelogs generated from your conventional commits, for both workspace and project-level releases.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Automate GitLab Releases
|
||||||
|
|
||||||
|
Nx Release can automate the creation of [GitLab releases](https://docs.gitlab.com/user/project/releases/) for you. GitLab releases are a great way to communicate the changes in your projects to your users.
|
||||||
|
|
||||||
|
<!-- Prettier will mess up the end tag of the callout causing it to capture all content that follows it -->
|
||||||
|
<!-- prettier-ignore-start -->
|
||||||
|
|
||||||
|
{% callout type="note" title="Authenticating with GitLab" %}
|
||||||
|
In order to be able to create the release on GitLab, you need to provide a valid token which can be used for authenticating with the GitLab API.
|
||||||
|
|
||||||
|
Nx release supports two main ways of doing this:
|
||||||
|
|
||||||
|
1. In all environments it will preferentially check for an environment variable (the environment variable can either be called `GITLAB_TOKEN` or `GL_TOKEN`).
|
||||||
|
2. In GitLab CI it will check for and use the automatically created GitLab token in the `CI_JOB_TOKEN` environment variable.
|
||||||
|
{% /callout %}
|
||||||
|
<!-- prettier-ignore-end -->
|
||||||
|
|
||||||
|
## GitLab Release Contents
|
||||||
|
|
||||||
|
When a GitLab release is created, it will include the changelog that Nx Release generates with entries based on the changes since the last release. Nx Release will parse the `feat` and `fix` type commits according to the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) specification and sort them into appropriate sections of the changelog.
|
||||||
|
|
||||||
|
## Enable Release Creation
|
||||||
|
|
||||||
|
To enable GitLab release creation for your workspace, set `release.changelog.workspaceChangelog.createRelease` to `'gitlab'` in `nx.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"release": {
|
||||||
|
"changelog": {
|
||||||
|
"workspaceChangelog": {
|
||||||
|
"createRelease": "gitlab"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Preview the Release
|
||||||
|
|
||||||
|
Use `nx release --dry-run` to preview the GitLab release instead of creating it. This allows you to see what the release will look like without pushing anything to GitLab.
|
||||||
|
|
||||||
|
## Disable File Creation
|
||||||
|
|
||||||
|
Since GitLab releases contain the changelog, you may wish to disable the generation and management of the local `CHANGELOG.md` file. To do this, set `release.changelog.workspaceChangelog.file` to `false` in `nx.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"release": {
|
||||||
|
"changelog": {
|
||||||
|
"workspaceChangelog": {
|
||||||
|
"file": false,
|
||||||
|
"createRelease": "gitlab"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: When configured this way, Nx Release will not delete existing changelog files, just ignore them.
|
||||||
|
|
||||||
|
## Project Level Changelogs
|
||||||
|
|
||||||
|
Nx Release supports creating GitLab releases for project level changelogs as well. This is particularly useful when [releasing projects independently](/recipes/nx-release/release-projects-independently). To enable this, set `release.changelog.projectChangelogs.createRelease` to `'gitlab'` in `nx.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"release": {
|
||||||
|
"changelog": {
|
||||||
|
"projectChangelogs": {
|
||||||
|
"createRelease": "gitlab"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
{% callout type="warning" title="Project and Workspace GitLab Releases" %}
|
||||||
|
Nx Release does not support creating GitLab releases for both project level changelogs and the workspace changelog. You will need to choose one or the other.
|
||||||
|
{% /callout %}
|
||||||
|
|
||||||
|
## Customizing the GitLab instance
|
||||||
|
|
||||||
|
If you are not using gitlab.com, and are instead using a self-hosted GitLab instance, you can use a configuration object instead of the string for "createRelease" to provide the relevant hostname, and optionally override the API base URL, although this is not typically needed as it will default to `https://${hostname}/api/v4`.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"release": {
|
||||||
|
"changelog": {
|
||||||
|
"workspaceChangelog": {
|
||||||
|
"createRelease": {
|
||||||
|
"provider": "gitlab",
|
||||||
|
"hostname": "gitlab.example.com"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
@ -32,7 +32,7 @@ There are a few options available to modify the default changelog renderer outpu
|
|||||||
"projectChangelogs": {
|
"projectChangelogs": {
|
||||||
"renderOptions": {
|
"renderOptions": {
|
||||||
"authors": true,
|
"authors": true,
|
||||||
"mapAuthorsToGitHubUsernames": true,
|
"applyUsernameToAuthors": true,
|
||||||
"commitReferences": true,
|
"commitReferences": true,
|
||||||
"versionTitleDate": true
|
"versionTitleDate": true
|
||||||
}
|
}
|
||||||
@ -46,12 +46,14 @@ There are a few options available to modify the default changelog renderer outpu
|
|||||||
|
|
||||||
Whether the commit authors should be added to the bottom of the changelog in a "Thank You" section. Defaults to `true`.
|
Whether the commit authors should be added to the bottom of the changelog in a "Thank You" section. Defaults to `true`.
|
||||||
|
|
||||||
#### `mapAuthorsToGitHubUsernames`
|
#### `applyUsernameToAuthors`
|
||||||
|
|
||||||
If authors is enabled, controls whether or not to try to map the authors to their GitHub usernames using https://ungh.cc (from https://github.com/unjs/ungh) and the email addresses found in the commits. Defaults to `true`.
|
If authors is enabled, controls whether or not to try to map the authors to their GitHub usernames using https://ungh.cc (from https://github.com/unjs/ungh) and the email addresses found in the commits. Defaults to `true`.
|
||||||
|
|
||||||
You should disable this option if you don't want to make any external requests to https://ungh.cc
|
You should disable this option if you don't want to make any external requests to https://ungh.cc
|
||||||
|
|
||||||
|
NOTE: Prior to Nx v21, this option was named `mapAuthorsToGitHubUsernames`.
|
||||||
|
|
||||||
#### `commitReferences`
|
#### `commitReferences`
|
||||||
|
|
||||||
Whether the commit references (such as commit and/or PR links) should be included in the changelog. Defaults to `true`.
|
Whether the commit references (such as commit and/or PR links) should be included in the changelog. Defaults to `true`.
|
||||||
@ -71,7 +73,7 @@ If you prefer a more minimalist changelog, you can set all the options to false,
|
|||||||
"projectChangelogs": {
|
"projectChangelogs": {
|
||||||
"renderOptions": {
|
"renderOptions": {
|
||||||
"authors": false,
|
"authors": false,
|
||||||
"mapAuthorsToGitHubUsernames": false,
|
"applyUsernameToAuthors": false,
|
||||||
"commitReferences": false,
|
"commitReferences": false,
|
||||||
"versionTitleDate": false
|
"versionTitleDate": false
|
||||||
}
|
}
|
||||||
|
|||||||
@ -83,6 +83,7 @@
|
|||||||
- [Configure Custom Registries](/recipes/nx-release/configure-custom-registries)
|
- [Configure Custom Registries](/recipes/nx-release/configure-custom-registries)
|
||||||
- [Publish in CI/CD](/recipes/nx-release/publish-in-ci-cd)
|
- [Publish in CI/CD](/recipes/nx-release/publish-in-ci-cd)
|
||||||
- [Automate GitHub Releases](/recipes/nx-release/automate-github-releases)
|
- [Automate GitHub Releases](/recipes/nx-release/automate-github-releases)
|
||||||
|
- [Automate GitLab Releases](/recipes/nx-release/automate-gitlab-releases)
|
||||||
- [Publish Rust Crates](/recipes/nx-release/publish-rust-crates)
|
- [Publish Rust Crates](/recipes/nx-release/publish-rust-crates)
|
||||||
- [Update Your Local Registry Setup to use Nx Release](/recipes/nx-release/update-local-registry-setup)
|
- [Update Your Local Registry Setup to use Nx Release](/recipes/nx-release/update-local-registry-setup)
|
||||||
- [Configure Changelog Format](/recipes/nx-release/configure-changelog-format)
|
- [Configure Changelog Format](/recipes/nx-release/configure-changelog-format)
|
||||||
|
|||||||
@ -29,11 +29,6 @@
|
|||||||
"implementation": "./src/migrations/update-20-0-1/use-legacy-cache",
|
"implementation": "./src/migrations/update-20-0-1/use-legacy-cache",
|
||||||
"x-repair-skip": true
|
"x-repair-skip": true
|
||||||
},
|
},
|
||||||
"release-version-config-changes": {
|
|
||||||
"version": "21.0.0-beta.1",
|
|
||||||
"description": "Updates release version config based on the breaking changes in Nx v21",
|
|
||||||
"implementation": "./src/migrations/update-21-0-0/release-version-config-changes"
|
|
||||||
},
|
|
||||||
"remove-legacy-cache": {
|
"remove-legacy-cache": {
|
||||||
"version": "21.0.0-beta.8",
|
"version": "21.0.0-beta.8",
|
||||||
"description": "Removes the legacy cache configuration from nx.json",
|
"description": "Removes the legacy cache configuration from nx.json",
|
||||||
@ -43,6 +38,16 @@
|
|||||||
"version": "21.0.0-beta.8",
|
"version": "21.0.0-beta.8",
|
||||||
"description": "Removes the legacy cache configuration from nx.json",
|
"description": "Removes the legacy cache configuration from nx.json",
|
||||||
"implementation": "./src/migrations/update-21-0-0/remove-custom-tasks-runner"
|
"implementation": "./src/migrations/update-21-0-0/remove-custom-tasks-runner"
|
||||||
|
},
|
||||||
|
"release-version-config-changes": {
|
||||||
|
"version": "21.0.0-beta.11",
|
||||||
|
"description": "Updates release version config based on the breaking changes in Nx v21",
|
||||||
|
"implementation": "./src/migrations/update-21-0-0/release-version-config-changes"
|
||||||
|
},
|
||||||
|
"release-changelog-config-changes": {
|
||||||
|
"version": "21.0.0-beta.11",
|
||||||
|
"description": "Updates release changelog config based on the breaking changes in Nx v21",
|
||||||
|
"implementation": "./src/migrations/update-21-0-0/release-changelog-config-changes"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import type { ChangelogChange } from '../../src/command-line/release/changelog';
|
import type { ChangelogChange } from '../../src/command-line/release/changelog';
|
||||||
import { DEFAULT_CONVENTIONAL_COMMITS_CONFIG } from '../../src/command-line/release/config/conventional-commits';
|
import { DEFAULT_CONVENTIONAL_COMMITS_CONFIG } from '../../src/command-line/release/config/conventional-commits';
|
||||||
|
import { GithubRemoteReleaseClient } from '../../src/command-line/release/utils/remote-release-clients/github';
|
||||||
import DefaultChangelogRenderer from './index';
|
import DefaultChangelogRenderer from './index';
|
||||||
|
|
||||||
jest.mock('../../src/project-graph/file-map-utils', () => ({
|
jest.mock('../../src/project-graph/file-map-utils', () => ({
|
||||||
@ -26,6 +27,15 @@ jest.mock('../../src/project-graph/file-map-utils', () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
describe('ChangelogRenderer', () => {
|
describe('ChangelogRenderer', () => {
|
||||||
|
const remoteReleaseClient = new GithubRemoteReleaseClient(
|
||||||
|
{
|
||||||
|
hostname: 'example.com',
|
||||||
|
slug: 'example/example',
|
||||||
|
apiBaseUrl: 'https://api.example.com',
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
null
|
||||||
|
);
|
||||||
const changes: ChangelogChange[] = [
|
const changes: ChangelogChange[] = [
|
||||||
{
|
{
|
||||||
shortHash: '4130f65',
|
shortHash: '4130f65',
|
||||||
@ -144,6 +154,7 @@ describe('ChangelogRenderer', () => {
|
|||||||
it('should generate markdown for all projects by organizing commits by type, then grouped by scope within the type (sorted alphabetically), then chronologically within the scope group', async () => {
|
it('should generate markdown for all projects by organizing commits by type, then grouped by scope within the type (sorted alphabetically), then chronologically within the scope group', async () => {
|
||||||
const renderer = new DefaultChangelogRenderer({
|
const renderer = new DefaultChangelogRenderer({
|
||||||
changes,
|
changes,
|
||||||
|
remoteReleaseClient,
|
||||||
changelogEntryVersion: 'v1.1.0',
|
changelogEntryVersion: 'v1.1.0',
|
||||||
project: null,
|
project: null,
|
||||||
isVersionPlans: false,
|
isVersionPlans: false,
|
||||||
@ -177,6 +188,7 @@ describe('ChangelogRenderer', () => {
|
|||||||
it('should not generate a Thank You section when changelogRenderOptions.authors is false', async () => {
|
it('should not generate a Thank You section when changelogRenderOptions.authors is false', async () => {
|
||||||
const renderer = new DefaultChangelogRenderer({
|
const renderer = new DefaultChangelogRenderer({
|
||||||
changes,
|
changes,
|
||||||
|
remoteReleaseClient,
|
||||||
// Major version, should use single # for generated heading
|
// Major version, should use single # for generated heading
|
||||||
changelogEntryVersion: 'v1.0.0',
|
changelogEntryVersion: 'v1.0.0',
|
||||||
project: null,
|
project: null,
|
||||||
@ -209,6 +221,7 @@ describe('ChangelogRenderer', () => {
|
|||||||
it('should generate markdown for the given project by organizing commits by type, then chronologically', async () => {
|
it('should generate markdown for the given project by organizing commits by type, then chronologically', async () => {
|
||||||
const otherOpts = {
|
const otherOpts = {
|
||||||
changes,
|
changes,
|
||||||
|
remoteReleaseClient,
|
||||||
changelogEntryVersion: 'v1.1.0',
|
changelogEntryVersion: 'v1.1.0',
|
||||||
entryWhenNoChanges: false as const,
|
entryWhenNoChanges: false as const,
|
||||||
isVersionPlans: false,
|
isVersionPlans: false,
|
||||||
@ -401,6 +414,7 @@ describe('ChangelogRenderer', () => {
|
|||||||
|
|
||||||
const otherOpts = {
|
const otherOpts = {
|
||||||
changes,
|
changes,
|
||||||
|
remoteReleaseClient,
|
||||||
changelogEntryVersion: 'v1.1.0',
|
changelogEntryVersion: 'v1.1.0',
|
||||||
entryWhenNoChanges: false as const,
|
entryWhenNoChanges: false as const,
|
||||||
isVersionPlans: false,
|
isVersionPlans: false,
|
||||||
@ -464,6 +478,7 @@ describe('ChangelogRenderer', () => {
|
|||||||
it('should respect the entryWhenNoChanges option for the workspace changelog', async () => {
|
it('should respect the entryWhenNoChanges option for the workspace changelog', async () => {
|
||||||
const otherOpts = {
|
const otherOpts = {
|
||||||
changes: [],
|
changes: [],
|
||||||
|
remoteReleaseClient,
|
||||||
changelogEntryVersion: 'v1.1.0',
|
changelogEntryVersion: 'v1.1.0',
|
||||||
project: null, // workspace changelog
|
project: null, // workspace changelog
|
||||||
isVersionPlans: false,
|
isVersionPlans: false,
|
||||||
@ -495,6 +510,7 @@ describe('ChangelogRenderer', () => {
|
|||||||
it('should respect the entryWhenNoChanges option for project changelogs', async () => {
|
it('should respect the entryWhenNoChanges option for project changelogs', async () => {
|
||||||
const otherOpts = {
|
const otherOpts = {
|
||||||
changes: [],
|
changes: [],
|
||||||
|
remoteReleaseClient,
|
||||||
changelogEntryVersion: 'v1.1.0',
|
changelogEntryVersion: 'v1.1.0',
|
||||||
project: 'pkg-a',
|
project: 'pkg-a',
|
||||||
isVersionPlans: false,
|
isVersionPlans: false,
|
||||||
@ -558,6 +574,7 @@ describe('ChangelogRenderer', () => {
|
|||||||
|
|
||||||
const markdown = await new DefaultChangelogRenderer({
|
const markdown = await new DefaultChangelogRenderer({
|
||||||
changes: changesWithOnlyRevert,
|
changes: changesWithOnlyRevert,
|
||||||
|
remoteReleaseClient,
|
||||||
changelogEntryVersion: 'v1.1.0',
|
changelogEntryVersion: 'v1.1.0',
|
||||||
project: null,
|
project: null,
|
||||||
isVersionPlans: false,
|
isVersionPlans: false,
|
||||||
@ -640,6 +657,7 @@ describe('ChangelogRenderer', () => {
|
|||||||
|
|
||||||
const markdown = await new DefaultChangelogRenderer({
|
const markdown = await new DefaultChangelogRenderer({
|
||||||
changes: changesWithRevertAndOriginal,
|
changes: changesWithRevertAndOriginal,
|
||||||
|
remoteReleaseClient,
|
||||||
changelogEntryVersion: 'v1.1.0',
|
changelogEntryVersion: 'v1.1.0',
|
||||||
project: null,
|
project: null,
|
||||||
isVersionPlans: false,
|
isVersionPlans: false,
|
||||||
@ -678,6 +696,7 @@ describe('ChangelogRenderer', () => {
|
|||||||
|
|
||||||
const markdown = await new DefaultChangelogRenderer({
|
const markdown = await new DefaultChangelogRenderer({
|
||||||
changes: [breakingChangeWithExplanation],
|
changes: [breakingChangeWithExplanation],
|
||||||
|
remoteReleaseClient,
|
||||||
changelogEntryVersion: 'v1.1.0',
|
changelogEntryVersion: 'v1.1.0',
|
||||||
project: null,
|
project: null,
|
||||||
isVersionPlans: false,
|
isVersionPlans: false,
|
||||||
@ -731,6 +750,7 @@ describe('ChangelogRenderer', () => {
|
|||||||
|
|
||||||
const markdown = await new DefaultChangelogRenderer({
|
const markdown = await new DefaultChangelogRenderer({
|
||||||
changes: [breakingChangeWithExplanation],
|
changes: [breakingChangeWithExplanation],
|
||||||
|
remoteReleaseClient,
|
||||||
changelogEntryVersion: 'v1.1.0',
|
changelogEntryVersion: 'v1.1.0',
|
||||||
project: null,
|
project: null,
|
||||||
isVersionPlans: false,
|
isVersionPlans: false,
|
||||||
@ -764,6 +784,7 @@ describe('ChangelogRenderer', () => {
|
|||||||
expect(
|
expect(
|
||||||
await new DefaultChangelogRenderer({
|
await new DefaultChangelogRenderer({
|
||||||
changes,
|
changes,
|
||||||
|
remoteReleaseClient,
|
||||||
changelogEntryVersion: 'v1.1.0',
|
changelogEntryVersion: 'v1.1.0',
|
||||||
entryWhenNoChanges: false as const,
|
entryWhenNoChanges: false as const,
|
||||||
changelogRenderOptions: {
|
changelogRenderOptions: {
|
||||||
@ -805,6 +826,7 @@ describe('ChangelogRenderer', () => {
|
|||||||
expect(
|
expect(
|
||||||
await new DefaultChangelogRenderer({
|
await new DefaultChangelogRenderer({
|
||||||
changes: [],
|
changes: [],
|
||||||
|
remoteReleaseClient,
|
||||||
changelogEntryVersion: 'v3.1.0',
|
changelogEntryVersion: 'v3.1.0',
|
||||||
entryWhenNoChanges:
|
entryWhenNoChanges:
|
||||||
'should not be printed because we have dependency bumps',
|
'should not be printed because we have dependency bumps',
|
||||||
@ -842,6 +864,7 @@ describe('ChangelogRenderer', () => {
|
|||||||
|
|
||||||
const renderer = new CustomChangelogRenderer({
|
const renderer = new CustomChangelogRenderer({
|
||||||
changes,
|
changes,
|
||||||
|
remoteReleaseClient,
|
||||||
changelogEntryVersion: 'v1.1.0',
|
changelogEntryVersion: 'v1.1.0',
|
||||||
project: null,
|
project: null,
|
||||||
isVersionPlans: false,
|
isVersionPlans: false,
|
||||||
|
|||||||
@ -1,15 +1,8 @@
|
|||||||
import { major } from 'semver';
|
import { major } from 'semver';
|
||||||
import { ChangelogChange } from '../../src/command-line/release/changelog';
|
import type { ChangelogChange } from '../../src/command-line/release/changelog';
|
||||||
import { NxReleaseConfig } from '../../src/command-line/release/config/config';
|
import type { NxReleaseConfig } from '../../src/command-line/release/config/config';
|
||||||
import { DEFAULT_CONVENTIONAL_COMMITS_CONFIG } from '../../src/command-line/release/config/conventional-commits';
|
import { DEFAULT_CONVENTIONAL_COMMITS_CONFIG } from '../../src/command-line/release/config/conventional-commits';
|
||||||
import {
|
import type { RemoteReleaseClient } from '../../src/command-line/release/utils/remote-release-clients/remote-release-client';
|
||||||
GithubRepoData,
|
|
||||||
formatReferences,
|
|
||||||
} from '../../src/command-line/release/utils/github';
|
|
||||||
|
|
||||||
// axios types and values don't seem to match
|
|
||||||
import _axios = require('axios');
|
|
||||||
const axios = _axios as any as (typeof _axios)['default'];
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The ChangelogRenderOptions are specific to each ChangelogRenderer implementation, and are taken
|
* The ChangelogRenderOptions are specific to each ChangelogRenderer implementation, and are taken
|
||||||
@ -42,7 +35,7 @@ export interface DefaultChangelogRenderOptions extends ChangelogRenderOptions {
|
|||||||
* using https://ungh.cc (from https://github.com/unjs/ungh) and the email addresses found in the commits.
|
* using https://ungh.cc (from https://github.com/unjs/ungh) and the email addresses found in the commits.
|
||||||
* Defaults to true.
|
* Defaults to true.
|
||||||
*/
|
*/
|
||||||
mapAuthorsToGitHubUsernames?: boolean;
|
applyUsernameToAuthors?: boolean;
|
||||||
/**
|
/**
|
||||||
* Whether or not the commit references (such as commit and/or PR links) should be included in the changelog.
|
* Whether or not the commit references (such as commit and/or PR links) should be included in the changelog.
|
||||||
* Defaults to true.
|
* Defaults to true.
|
||||||
@ -63,13 +56,13 @@ export default class DefaultChangelogRenderer {
|
|||||||
protected changelogRenderOptions: DefaultChangelogRenderOptions;
|
protected changelogRenderOptions: DefaultChangelogRenderOptions;
|
||||||
protected isVersionPlans: boolean;
|
protected isVersionPlans: boolean;
|
||||||
protected dependencyBumps?: DependencyBump[];
|
protected dependencyBumps?: DependencyBump[];
|
||||||
protected repoData?: GithubRepoData;
|
|
||||||
protected conventionalCommitsConfig:
|
protected conventionalCommitsConfig:
|
||||||
| NxReleaseConfig['conventionalCommits']
|
| NxReleaseConfig['conventionalCommits']
|
||||||
| null;
|
| null;
|
||||||
protected relevantChanges: ChangelogChange[];
|
protected relevantChanges: ChangelogChange[];
|
||||||
protected breakingChanges: string[];
|
protected breakingChanges: string[];
|
||||||
protected additionalChangesForAuthorsSection: ChangelogChange[];
|
protected additionalChangesForAuthorsSection: ChangelogChange[];
|
||||||
|
protected remoteReleaseClient: RemoteReleaseClient<unknown>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A ChangelogRenderer class takes in the determined changes and other relevant metadata
|
* A ChangelogRenderer class takes in the determined changes and other relevant metadata
|
||||||
@ -83,8 +76,8 @@ export default class DefaultChangelogRenderer {
|
|||||||
* @param {boolean} config.isVersionPlans Whether or not Nx release version plans are the source of truth for the changelog entry
|
* @param {boolean} config.isVersionPlans Whether or not Nx release version plans are the source of truth for the changelog entry
|
||||||
* @param {ChangelogRenderOptions} config.changelogRenderOptions The options specific to the ChangelogRenderer implementation
|
* @param {ChangelogRenderOptions} config.changelogRenderOptions The options specific to the ChangelogRenderer implementation
|
||||||
* @param {DependencyBump[]} config.dependencyBumps Optional list of additional dependency bumps that occurred as part of the release, outside of the change data
|
* @param {DependencyBump[]} config.dependencyBumps Optional list of additional dependency bumps that occurred as part of the release, outside of the change data
|
||||||
* @param {GithubRepoData} config.repoData Resolved data for the current GitHub repository
|
|
||||||
* @param {NxReleaseConfig['conventionalCommits'] | null} config.conventionalCommitsConfig The configuration for conventional commits, or null if version plans are being used
|
* @param {NxReleaseConfig['conventionalCommits'] | null} config.conventionalCommitsConfig The configuration for conventional commits, or null if version plans are being used
|
||||||
|
* @param {RemoteReleaseClient} config.remoteReleaseClient The remote release client to use for formatting references
|
||||||
*/
|
*/
|
||||||
constructor(config: {
|
constructor(config: {
|
||||||
changes: ChangelogChange[];
|
changes: ChangelogChange[];
|
||||||
@ -94,8 +87,8 @@ export default class DefaultChangelogRenderer {
|
|||||||
isVersionPlans: boolean;
|
isVersionPlans: boolean;
|
||||||
changelogRenderOptions: DefaultChangelogRenderOptions;
|
changelogRenderOptions: DefaultChangelogRenderOptions;
|
||||||
dependencyBumps?: DependencyBump[];
|
dependencyBumps?: DependencyBump[];
|
||||||
repoData?: GithubRepoData;
|
|
||||||
conventionalCommitsConfig: NxReleaseConfig['conventionalCommits'] | null;
|
conventionalCommitsConfig: NxReleaseConfig['conventionalCommits'] | null;
|
||||||
|
remoteReleaseClient: RemoteReleaseClient<unknown>;
|
||||||
}) {
|
}) {
|
||||||
this.changes = this.filterChanges(config.changes, config.project);
|
this.changes = this.filterChanges(config.changes, config.project);
|
||||||
this.changelogEntryVersion = config.changelogEntryVersion;
|
this.changelogEntryVersion = config.changelogEntryVersion;
|
||||||
@ -104,8 +97,8 @@ export default class DefaultChangelogRenderer {
|
|||||||
this.isVersionPlans = config.isVersionPlans;
|
this.isVersionPlans = config.isVersionPlans;
|
||||||
this.changelogRenderOptions = config.changelogRenderOptions;
|
this.changelogRenderOptions = config.changelogRenderOptions;
|
||||||
this.dependencyBumps = config.dependencyBumps;
|
this.dependencyBumps = config.dependencyBumps;
|
||||||
this.repoData = config.repoData;
|
|
||||||
this.conventionalCommitsConfig = config.conventionalCommitsConfig;
|
this.conventionalCommitsConfig = config.conventionalCommitsConfig;
|
||||||
|
this.remoteReleaseClient = config.remoteReleaseClient;
|
||||||
|
|
||||||
this.relevantChanges = [];
|
this.relevantChanges = [];
|
||||||
this.breakingChanges = [];
|
this.breakingChanges = [];
|
||||||
@ -341,7 +334,10 @@ export default class DefaultChangelogRenderer {
|
|||||||
|
|
||||||
protected async renderAuthors(): Promise<string[]> {
|
protected async renderAuthors(): Promise<string[]> {
|
||||||
const markdownLines: string[] = [];
|
const markdownLines: string[] = [];
|
||||||
const _authors = new Map<string, { email: Set<string>; github?: string }>();
|
const _authors = new Map<
|
||||||
|
string,
|
||||||
|
{ email: Set<string>; username?: string }
|
||||||
|
>();
|
||||||
|
|
||||||
for (const change of [
|
for (const change of [
|
||||||
...this.relevantChanges,
|
...this.relevantChanges,
|
||||||
@ -365,34 +361,12 @@ export default class DefaultChangelogRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
this.repoData &&
|
this.remoteReleaseClient.getRemoteRepoData() &&
|
||||||
this.changelogRenderOptions.mapAuthorsToGitHubUsernames
|
this.changelogRenderOptions.applyUsernameToAuthors &&
|
||||||
|
// TODO: Explore if it is possible to support GitLab username resolution
|
||||||
|
this.remoteReleaseClient.remoteReleaseProviderName === 'GitHub'
|
||||||
) {
|
) {
|
||||||
await Promise.all(
|
await this.remoteReleaseClient.applyUsernameToAuthors(_authors);
|
||||||
[..._authors.keys()].map(async (authorName) => {
|
|
||||||
const meta = _authors.get(authorName);
|
|
||||||
for (const email of meta.email) {
|
|
||||||
if (email.endsWith('@users.noreply.github.com')) {
|
|
||||||
const match = email.match(
|
|
||||||
/^(\d+\+)?([^@]+)@users\.noreply\.github\.com$/
|
|
||||||
);
|
|
||||||
if (match && match[2]) {
|
|
||||||
meta.github = match[2];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const { data } = await axios
|
|
||||||
.get<any, { data?: { user?: { username: string } } }>(
|
|
||||||
`https://ungh.cc/users/find/${email}`
|
|
||||||
)
|
|
||||||
.catch(() => ({ data: { user: null } }));
|
|
||||||
if (data?.user) {
|
|
||||||
meta.github = data.user.username;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const authors = [..._authors.entries()].map((e) => ({
|
const authors = [..._authors.entries()].map((e) => ({
|
||||||
@ -408,8 +382,8 @@ export default class DefaultChangelogRenderer {
|
|||||||
...authors
|
...authors
|
||||||
.sort((a, b) => a.name.localeCompare(b.name))
|
.sort((a, b) => a.name.localeCompare(b.name))
|
||||||
.map((i) => {
|
.map((i) => {
|
||||||
const github = i.github ? ` @${i.github}` : '';
|
const username = i.username ? ` @${i.username}` : '';
|
||||||
return `- ${i.name}${github}`;
|
return `- ${i.name}${username}`;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -437,8 +411,13 @@ export default class DefaultChangelogRenderer {
|
|||||||
? `**${change.scope.trim()}:** `
|
? `**${change.scope.trim()}:** `
|
||||||
: '') +
|
: '') +
|
||||||
description;
|
description;
|
||||||
if (this.repoData && this.changelogRenderOptions.commitReferences) {
|
if (
|
||||||
changeLine += formatReferences(change.githubReferences, this.repoData);
|
this.remoteReleaseClient.getRemoteRepoData() &&
|
||||||
|
this.changelogRenderOptions.commitReferences
|
||||||
|
) {
|
||||||
|
changeLine += this.remoteReleaseClient.formatReferences(
|
||||||
|
change.githubReferences
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (extraLinesStr) {
|
if (extraLinesStr) {
|
||||||
changeLine += '\n\n' + extraLinesStr;
|
changeLine += '\n\n' + extraLinesStr;
|
||||||
|
|||||||
@ -854,7 +854,7 @@
|
|||||||
"oneOf": [
|
"oneOf": [
|
||||||
{
|
{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": ["github"]
|
"enum": ["github", "gitlab"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
@ -900,7 +900,7 @@
|
|||||||
"properties": {
|
"properties": {
|
||||||
"provider": {
|
"provider": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": ["github-enterprise-server"]
|
"enum": ["github-enterprise-server", "gitlab"]
|
||||||
},
|
},
|
||||||
"hostname": {
|
"hostname": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@ -908,7 +908,7 @@
|
|||||||
},
|
},
|
||||||
"apiBaseUrl": {
|
"apiBaseUrl": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "The base URL for the relevant VCS provider API. If not set, this will default to `https://${hostname}/api/v3`"
|
"description": "The base URL for the relevant VCS provider API. If not set, this will default to `https://${hostname}/api/v3` (github) or `https://${hostname}/api/v4` (gitlab)."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["provider", "hostname"]
|
"required": ["provider", "hostname"]
|
||||||
|
|||||||
@ -8,7 +8,6 @@ import { NxReleaseConfiguration, readNxJson } from '../../config/nx-json';
|
|||||||
import {
|
import {
|
||||||
FileData,
|
FileData,
|
||||||
ProjectFileMap,
|
ProjectFileMap,
|
||||||
ProjectGraph,
|
|
||||||
ProjectGraphProjectNode,
|
ProjectGraphProjectNode,
|
||||||
} from '../../config/project-graph';
|
} from '../../config/project-graph';
|
||||||
import { FsTree, Tree } from '../../generators/tree';
|
import { FsTree, Tree } from '../../generators/tree';
|
||||||
@ -26,8 +25,8 @@ import { workspaceRoot } from '../../utils/workspace-root';
|
|||||||
import { ChangelogOptions } from './command-object';
|
import { ChangelogOptions } from './command-object';
|
||||||
import {
|
import {
|
||||||
NxReleaseConfig,
|
NxReleaseConfig,
|
||||||
|
ResolvedCreateRemoteReleaseProvider,
|
||||||
createNxReleaseConfig,
|
createNxReleaseConfig,
|
||||||
defaultCreateReleaseProvider,
|
|
||||||
handleNxReleaseConfigError,
|
handleNxReleaseConfigError,
|
||||||
} from './config/config';
|
} from './config/config';
|
||||||
import { deepMergeJson } from './config/deep-merge-json';
|
import { deepMergeJson } from './config/deep-merge-json';
|
||||||
@ -55,11 +54,11 @@ import {
|
|||||||
parseCommits,
|
parseCommits,
|
||||||
parseGitCommit,
|
parseGitCommit,
|
||||||
} from './utils/git';
|
} from './utils/git';
|
||||||
import { createOrUpdateGithubRelease, getGitHubRepoData } from './utils/github';
|
|
||||||
import { launchEditor } from './utils/launch-editor';
|
import { launchEditor } from './utils/launch-editor';
|
||||||
import { parseChangelogMarkdown } from './utils/markdown';
|
import { parseChangelogMarkdown } from './utils/markdown';
|
||||||
import { printAndFlushChanges } from './utils/print-changes';
|
import { printAndFlushChanges } from './utils/print-changes';
|
||||||
import { printConfigAndExit } from './utils/print-config';
|
import { printConfigAndExit } from './utils/print-config';
|
||||||
|
import { createRemoteReleaseClient } from './utils/remote-release-clients/remote-release-client';
|
||||||
import { resolveChangelogRenderer } from './utils/resolve-changelog-renderer';
|
import { resolveChangelogRenderer } from './utils/resolve-changelog-renderer';
|
||||||
import { resolveNxJsonConfigErrorMessage } from './utils/resolve-nx-json-error-message';
|
import { resolveNxJsonConfigErrorMessage } from './utils/resolve-nx-json-error-message';
|
||||||
import {
|
import {
|
||||||
@ -76,11 +75,13 @@ export interface NxReleaseChangelogResult {
|
|||||||
workspaceChangelog?: {
|
workspaceChangelog?: {
|
||||||
releaseVersion: ReleaseVersion;
|
releaseVersion: ReleaseVersion;
|
||||||
contents: string;
|
contents: string;
|
||||||
|
postGitTask: PostGitTask | null;
|
||||||
};
|
};
|
||||||
projectChangelogs?: {
|
projectChangelogs?: {
|
||||||
[projectName: string]: {
|
[projectName: string]: {
|
||||||
releaseVersion: ReleaseVersion;
|
releaseVersion: ReleaseVersion;
|
||||||
contents: string;
|
contents: string;
|
||||||
|
postGitTask: PostGitTask | null;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -98,7 +99,7 @@ export interface ChangelogChange {
|
|||||||
revertedHashes?: string[];
|
revertedHashes?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
type PostGitTask = (latestCommit: string) => Promise<void>;
|
export type PostGitTask = (latestCommit: string) => Promise<void>;
|
||||||
|
|
||||||
export const releaseChangelogCLIHandler = (args: ChangelogOptions) =>
|
export const releaseChangelogCLIHandler = (args: ChangelogOptions) =>
|
||||||
handleErrors(args.verbose, () => createAPI({})(args));
|
handleErrors(args.verbose, () => createAPI({})(args));
|
||||||
@ -392,37 +393,14 @@ export function createAPI(overrideReleaseConfig: NxReleaseConfiguration) {
|
|||||||
const workspaceChangelog = await generateChangelogForWorkspace({
|
const workspaceChangelog = await generateChangelogForWorkspace({
|
||||||
tree,
|
tree,
|
||||||
args,
|
args,
|
||||||
projectGraph,
|
|
||||||
nxReleaseConfig,
|
nxReleaseConfig,
|
||||||
workspaceChangelogVersion,
|
workspaceChangelogVersion,
|
||||||
changes: workspaceChangelogChanges,
|
changes: workspaceChangelogChanges,
|
||||||
// TODO(v22): remove this after the changelog renderer is refactored to remove coupling with git commits
|
|
||||||
commits: filterHiddenCommits(
|
|
||||||
workspaceChangelogCommits,
|
|
||||||
nxReleaseConfig.conventionalCommits
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (
|
// Add the post git task (e.g. create a remote release) for the workspace changelog, if applicable
|
||||||
workspaceChangelog &&
|
if (workspaceChangelog && workspaceChangelog.postGitTask) {
|
||||||
shouldCreateGitHubRelease(
|
postGitTasks.push(workspaceChangelog.postGitTask);
|
||||||
nxReleaseConfig.changelog.workspaceChangelog,
|
|
||||||
args.createRelease
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
postGitTasks.push(async (latestCommit) => {
|
|
||||||
output.logSingleLine(`Creating GitHub Release`);
|
|
||||||
|
|
||||||
await createOrUpdateGithubRelease(
|
|
||||||
nxReleaseConfig.changelog.workspaceChangelog
|
|
||||||
? nxReleaseConfig.changelog.workspaceChangelog.createRelease
|
|
||||||
: defaultCreateReleaseProvider,
|
|
||||||
workspaceChangelog.releaseVersion,
|
|
||||||
workspaceChangelog.contents,
|
|
||||||
latestCommit,
|
|
||||||
{ dryRun: args.dryRun }
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -620,31 +598,16 @@ export function createAPI(overrideReleaseConfig: NxReleaseConfiguration) {
|
|||||||
projectToAdditionalDependencyBumps,
|
projectToAdditionalDependencyBumps,
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const [projectName, projectChangelog] of Object.entries(
|
if (projectChangelogs) {
|
||||||
projectChangelogs
|
for (const [projectName, projectChangelog] of Object.entries(
|
||||||
)) {
|
projectChangelogs
|
||||||
if (
|
)) {
|
||||||
projectChangelogs &&
|
// Add the post git task (e.g. create a remote release) for the project changelog, if applicable
|
||||||
shouldCreateGitHubRelease(
|
if (projectChangelog.postGitTask) {
|
||||||
releaseGroup.changelog,
|
postGitTasks.push(projectChangelog.postGitTask);
|
||||||
args.createRelease
|
}
|
||||||
)
|
allProjectChangelogs[projectName] = projectChangelog;
|
||||||
) {
|
|
||||||
postGitTasks.push(async (latestCommit) => {
|
|
||||||
output.logSingleLine(`Creating GitHub Release`);
|
|
||||||
|
|
||||||
await createOrUpdateGithubRelease(
|
|
||||||
releaseGroup.changelog
|
|
||||||
? releaseGroup.changelog.createRelease
|
|
||||||
: defaultCreateReleaseProvider,
|
|
||||||
projectChangelog.releaseVersion,
|
|
||||||
projectChangelog.contents,
|
|
||||||
latestCommit,
|
|
||||||
{ dryRun: args.dryRun }
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
allProjectChangelogs[projectName] = projectChangelog;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -765,31 +728,16 @@ export function createAPI(overrideReleaseConfig: NxReleaseConfiguration) {
|
|||||||
projectToAdditionalDependencyBumps,
|
projectToAdditionalDependencyBumps,
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const [projectName, projectChangelog] of Object.entries(
|
if (projectChangelogs) {
|
||||||
projectChangelogs
|
for (const [projectName, projectChangelog] of Object.entries(
|
||||||
)) {
|
projectChangelogs
|
||||||
if (
|
)) {
|
||||||
projectChangelogs &&
|
// Add the post git task (e.g. create a remote release) for the project changelog, if applicable
|
||||||
shouldCreateGitHubRelease(
|
if (projectChangelog.postGitTask) {
|
||||||
releaseGroup.changelog,
|
postGitTasks.push(projectChangelog.postGitTask);
|
||||||
args.createRelease
|
}
|
||||||
)
|
allProjectChangelogs[projectName] = projectChangelog;
|
||||||
) {
|
|
||||||
postGitTasks.push(async (latestCommit) => {
|
|
||||||
output.logSingleLine(`Creating GitHub Release`);
|
|
||||||
|
|
||||||
await createOrUpdateGithubRelease(
|
|
||||||
releaseGroup.changelog
|
|
||||||
? releaseGroup.changelog.createRelease
|
|
||||||
: defaultCreateReleaseProvider,
|
|
||||||
projectChangelog.releaseVersion,
|
|
||||||
projectChangelog.contents,
|
|
||||||
latestCommit,
|
|
||||||
{ dryRun: args.dryRun }
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
allProjectChangelogs[projectName] = projectChangelog;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -873,6 +821,20 @@ function resolveChangelogVersions(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Can be overridden to something more specific as we resolve the remote release client within nested logic
|
||||||
|
let remoteReleaseProviderName: undefined | string;
|
||||||
|
|
||||||
|
// If already set, and not the same as the remote release client, append
|
||||||
|
function applyRemoteReleaseProviderName(newRemoteReleaseProviderName: string) {
|
||||||
|
if (remoteReleaseProviderName) {
|
||||||
|
if (remoteReleaseProviderName !== newRemoteReleaseProviderName) {
|
||||||
|
remoteReleaseProviderName = `${remoteReleaseProviderName}/${newRemoteReleaseProviderName}`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
remoteReleaseProviderName = newRemoteReleaseProviderName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function applyChangesAndExit(
|
async function applyChangesAndExit(
|
||||||
args: ChangelogOptions,
|
args: ChangelogOptions,
|
||||||
nxReleaseConfig: NxReleaseConfig,
|
nxReleaseConfig: NxReleaseConfig,
|
||||||
@ -902,22 +864,25 @@ async function applyChangesAndExit(
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!postGitTasks.length) {
|
if (!postGitTasks.length) {
|
||||||
// no GitHub releases to create so we can just exit
|
// No post git tasks (e.g. remote release creation) to perform so we can just exit
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isCI()) {
|
if (isCI()) {
|
||||||
output.warn({
|
output.warn({
|
||||||
title: `Skipped GitHub release creation because no changes were detected for any changelog files.`,
|
title: `Skipped ${
|
||||||
|
remoteReleaseProviderName ?? 'remote'
|
||||||
|
} release creation because no changes were detected for any changelog files.`,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// prompt the user to see if they want to create a GitHub release anyway
|
/**
|
||||||
// we know that the user has configured GitHub releases because we have postGitTasks
|
* Prompt the user to see if they want to create a remote release anyway.
|
||||||
const shouldCreateGitHubReleaseAnyway = await promptForGitHubRelease();
|
* We know that the user has configured remote releases because we have postGitTasks.
|
||||||
|
*/
|
||||||
if (!shouldCreateGitHubReleaseAnyway) {
|
const shouldCreateRemoteReleaseAnyway = await promptForRemoteRelease();
|
||||||
|
if (!shouldCreateRemoteReleaseAnyway) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1020,20 +985,16 @@ async function applyChangesAndExit(
|
|||||||
async function generateChangelogForWorkspace({
|
async function generateChangelogForWorkspace({
|
||||||
tree,
|
tree,
|
||||||
args,
|
args,
|
||||||
projectGraph,
|
|
||||||
nxReleaseConfig,
|
nxReleaseConfig,
|
||||||
workspaceChangelogVersion,
|
workspaceChangelogVersion,
|
||||||
changes,
|
changes,
|
||||||
commits,
|
|
||||||
}: {
|
}: {
|
||||||
tree: Tree;
|
tree: Tree;
|
||||||
args: ChangelogOptions;
|
args: ChangelogOptions;
|
||||||
projectGraph: ProjectGraph;
|
|
||||||
nxReleaseConfig: NxReleaseConfig;
|
nxReleaseConfig: NxReleaseConfig;
|
||||||
workspaceChangelogVersion: (string | null) | undefined;
|
workspaceChangelogVersion: (string | null) | undefined;
|
||||||
changes: ChangelogChange[];
|
changes: ChangelogChange[];
|
||||||
commits: GitCommit[];
|
}): Promise<NxReleaseChangelogResult['workspaceChangelog'] | undefined> {
|
||||||
}): Promise<NxReleaseChangelogResult['workspaceChangelog']> {
|
|
||||||
const config = nxReleaseConfig.changelog.workspaceChangelog;
|
const config = nxReleaseConfig.changelog.workspaceChangelog;
|
||||||
// The entire feature is disabled at the workspace level, exit early
|
// The entire feature is disabled at the workspace level, exit early
|
||||||
if (config === false) {
|
if (config === false) {
|
||||||
@ -1108,17 +1069,23 @@ async function generateChangelogForWorkspace({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const githubRepoData = getGitHubRepoData(gitRemote, config.createRelease);
|
const remoteReleaseClient = await createRemoteReleaseClient(
|
||||||
|
config.createRelease as unknown as
|
||||||
|
| false
|
||||||
|
| ResolvedCreateRemoteReleaseProvider,
|
||||||
|
gitRemote
|
||||||
|
);
|
||||||
|
applyRemoteReleaseProviderName(remoteReleaseClient.remoteReleaseProviderName);
|
||||||
|
|
||||||
const changelogRenderer = new ChangelogRendererClass({
|
const changelogRenderer = new ChangelogRendererClass({
|
||||||
changes,
|
changes,
|
||||||
changelogEntryVersion: releaseVersion.rawVersion,
|
changelogEntryVersion: releaseVersion.rawVersion,
|
||||||
project: null,
|
project: null,
|
||||||
isVersionPlans: false,
|
isVersionPlans: false,
|
||||||
repoData: githubRepoData,
|
|
||||||
entryWhenNoChanges: config.entryWhenNoChanges,
|
entryWhenNoChanges: config.entryWhenNoChanges,
|
||||||
changelogRenderOptions: config.renderOptions,
|
changelogRenderOptions: config.renderOptions,
|
||||||
conventionalCommitsConfig: nxReleaseConfig.conventionalCommits,
|
conventionalCommitsConfig: nxReleaseConfig.conventionalCommits,
|
||||||
|
remoteReleaseClient,
|
||||||
});
|
});
|
||||||
let contents = await changelogRenderer.render();
|
let contents = await changelogRenderer.render();
|
||||||
|
|
||||||
@ -1170,9 +1137,15 @@ async function generateChangelogForWorkspace({
|
|||||||
printAndFlushChanges(tree, !!dryRun, 3, false, noDiffInChangelogMessage);
|
printAndFlushChanges(tree, !!dryRun, 3, false, noDiffInChangelogMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const postGitTask: PostGitTask | null =
|
||||||
|
args.createRelease !== false && config.createRelease
|
||||||
|
? remoteReleaseClient.createPostGitTask(releaseVersion, contents, dryRun)
|
||||||
|
: null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
releaseVersion,
|
releaseVersion,
|
||||||
contents,
|
contents,
|
||||||
|
postGitTask,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1194,7 +1167,7 @@ async function generateChangelogForProjects({
|
|||||||
projects: ProjectGraphProjectNode[];
|
projects: ProjectGraphProjectNode[];
|
||||||
nxReleaseConfig: NxReleaseConfig;
|
nxReleaseConfig: NxReleaseConfig;
|
||||||
projectToAdditionalDependencyBumps: Map<string, DependencyBump[]>;
|
projectToAdditionalDependencyBumps: Map<string, DependencyBump[]>;
|
||||||
}): Promise<NxReleaseChangelogResult['projectChangelogs']> {
|
}): Promise<NxReleaseChangelogResult['projectChangelogs'] | undefined> {
|
||||||
const config = releaseGroup.changelog;
|
const config = releaseGroup.changelog;
|
||||||
// The entire feature is disabled at the release group level, exit early
|
// The entire feature is disabled at the release group level, exit early
|
||||||
if (config === false) {
|
if (config === false) {
|
||||||
@ -1209,6 +1182,15 @@ async function generateChangelogForProjects({
|
|||||||
|
|
||||||
const ChangelogRendererClass = resolveChangelogRenderer(config.renderer);
|
const ChangelogRendererClass = resolveChangelogRenderer(config.renderer);
|
||||||
|
|
||||||
|
// Maximum of one remote release client per release group
|
||||||
|
const remoteReleaseClient = await createRemoteReleaseClient(
|
||||||
|
config.createRelease as unknown as
|
||||||
|
| false
|
||||||
|
| ResolvedCreateRemoteReleaseProvider,
|
||||||
|
gitRemote
|
||||||
|
);
|
||||||
|
applyRemoteReleaseProviderName(remoteReleaseClient.remoteReleaseProviderName);
|
||||||
|
|
||||||
const projectChangelogs: NxReleaseChangelogResult['projectChangelogs'] = {};
|
const projectChangelogs: NxReleaseChangelogResult['projectChangelogs'] = {};
|
||||||
|
|
||||||
for (const project of projects) {
|
for (const project of projects) {
|
||||||
@ -1247,13 +1229,10 @@ async function generateChangelogForProjects({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const githubRepoData = getGitHubRepoData(gitRemote, config.createRelease);
|
|
||||||
|
|
||||||
const changelogRenderer = new ChangelogRendererClass({
|
const changelogRenderer = new ChangelogRendererClass({
|
||||||
changes,
|
changes,
|
||||||
changelogEntryVersion: releaseVersion.rawVersion,
|
changelogEntryVersion: releaseVersion.rawVersion,
|
||||||
project: project.name,
|
project: project.name,
|
||||||
repoData: githubRepoData,
|
|
||||||
entryWhenNoChanges:
|
entryWhenNoChanges:
|
||||||
typeof config.entryWhenNoChanges === 'string'
|
typeof config.entryWhenNoChanges === 'string'
|
||||||
? interpolate(config.entryWhenNoChanges, {
|
? interpolate(config.entryWhenNoChanges, {
|
||||||
@ -1268,6 +1247,7 @@ async function generateChangelogForProjects({
|
|||||||
? null
|
? null
|
||||||
: nxReleaseConfig.conventionalCommits,
|
: nxReleaseConfig.conventionalCommits,
|
||||||
dependencyBumps: projectToAdditionalDependencyBumps.get(project.name),
|
dependencyBumps: projectToAdditionalDependencyBumps.get(project.name),
|
||||||
|
remoteReleaseClient,
|
||||||
});
|
});
|
||||||
let contents = await changelogRenderer.render();
|
let contents = await changelogRenderer.render();
|
||||||
|
|
||||||
@ -1326,9 +1306,19 @@ async function generateChangelogForProjects({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const postGitTask: PostGitTask | null =
|
||||||
|
args.createRelease !== false && config.createRelease
|
||||||
|
? remoteReleaseClient.createPostGitTask(
|
||||||
|
releaseVersion,
|
||||||
|
contents,
|
||||||
|
dryRun
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
|
||||||
projectChangelogs[project.name] = {
|
projectChangelogs[project.name] = {
|
||||||
releaseVersion,
|
releaseVersion,
|
||||||
contents,
|
contents,
|
||||||
|
postGitTask,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1375,48 +1365,14 @@ function filterHiddenChanges(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(v22): remove this after the changelog renderer is refactored to remove coupling with git commits
|
async function promptForRemoteRelease(): Promise<boolean> {
|
||||||
function filterHiddenCommits(
|
|
||||||
commits: GitCommit[],
|
|
||||||
conventionalCommitsConfig: NxReleaseConfig['conventionalCommits']
|
|
||||||
): GitCommit[] {
|
|
||||||
if (!commits) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
return commits.filter((commit) => {
|
|
||||||
const type = commit.type;
|
|
||||||
|
|
||||||
const typeConfig = conventionalCommitsConfig.types[type];
|
|
||||||
if (!typeConfig) {
|
|
||||||
// don't include commits with unknown types
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return !typeConfig.changelog.hidden;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function shouldCreateGitHubRelease(
|
|
||||||
changelogConfig:
|
|
||||||
| NxReleaseConfig['changelog']['workspaceChangelog']
|
|
||||||
| NxReleaseConfig['changelog']['projectChangelogs']
|
|
||||||
| NxReleaseConfig['groups'][number]['changelog'],
|
|
||||||
createReleaseArg: ChangelogOptions['createRelease'] | undefined = undefined
|
|
||||||
): boolean {
|
|
||||||
if (createReleaseArg !== undefined) {
|
|
||||||
return createReleaseArg === 'github';
|
|
||||||
}
|
|
||||||
if (changelogConfig === false) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return changelogConfig.createRelease !== false;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function promptForGitHubRelease(): Promise<boolean> {
|
|
||||||
try {
|
try {
|
||||||
const result = await prompt<{ confirmation: boolean }>([
|
const result = await prompt<{ confirmation: boolean }>([
|
||||||
{
|
{
|
||||||
name: 'confirmation',
|
name: 'confirmation',
|
||||||
message: 'Do you want to create a GitHub release anyway?',
|
message: `Do you want to create a ${
|
||||||
|
remoteReleaseProviderName ?? 'remote'
|
||||||
|
} release anyway?`,
|
||||||
type: 'confirm',
|
type: 'confirm',
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|||||||
@ -64,7 +64,7 @@ export type ChangelogOptions = NxReleaseArgs &
|
|||||||
to?: string;
|
to?: string;
|
||||||
from?: string;
|
from?: string;
|
||||||
interactive?: string;
|
interactive?: string;
|
||||||
createRelease?: false | 'github';
|
createRelease?: false | 'github' | 'gitlab';
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PublishOptions = NxReleaseArgs &
|
export type PublishOptions = NxReleaseArgs &
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -28,6 +28,8 @@ import { output } from '../../../utils/output';
|
|||||||
import { PackageJson } from '../../../utils/package-json';
|
import { PackageJson } from '../../../utils/package-json';
|
||||||
import { normalizePath } from '../../../utils/path';
|
import { normalizePath } from '../../../utils/path';
|
||||||
import { workspaceRoot } from '../../../utils/workspace-root';
|
import { workspaceRoot } from '../../../utils/workspace-root';
|
||||||
|
import { defaultCreateReleaseProvider as defaultGitHubCreateReleaseProvider } from '../utils/remote-release-clients/github';
|
||||||
|
import { defaultCreateReleaseProvider as defaultGitLabCreateReleaseProvider } from '../utils/remote-release-clients/gitlab';
|
||||||
import { resolveChangelogRenderer } from '../utils/resolve-changelog-renderer';
|
import { resolveChangelogRenderer } from '../utils/resolve-changelog-renderer';
|
||||||
import { resolveNxJsonConfigErrorMessage } from '../utils/resolve-nx-json-error-message';
|
import { resolveNxJsonConfigErrorMessage } from '../utils/resolve-nx-json-error-message';
|
||||||
import { DEFAULT_CONVENTIONAL_COMMITS_CONFIG } from './conventional-commits';
|
import { DEFAULT_CONVENTIONAL_COMMITS_CONFIG } from './conventional-commits';
|
||||||
@ -308,7 +310,7 @@ export async function createNxReleaseConfig(
|
|||||||
renderer: defaultRendererPath,
|
renderer: defaultRendererPath,
|
||||||
renderOptions: {
|
renderOptions: {
|
||||||
authors: true,
|
authors: true,
|
||||||
mapAuthorsToGitHubUsernames: true,
|
applyUsernameToAuthors: true,
|
||||||
commitReferences: true,
|
commitReferences: true,
|
||||||
versionTitleDate: true,
|
versionTitleDate: true,
|
||||||
},
|
},
|
||||||
@ -323,7 +325,7 @@ export async function createNxReleaseConfig(
|
|||||||
renderer: defaultRendererPath,
|
renderer: defaultRendererPath,
|
||||||
renderOptions: {
|
renderOptions: {
|
||||||
authors: true,
|
authors: true,
|
||||||
mapAuthorsToGitHubUsernames: true,
|
applyUsernameToAuthors: true,
|
||||||
commitReferences: true,
|
commitReferences: true,
|
||||||
versionTitleDate: true,
|
versionTitleDate: true,
|
||||||
},
|
},
|
||||||
@ -374,7 +376,7 @@ export async function createNxReleaseConfig(
|
|||||||
renderer: defaultRendererPath,
|
renderer: defaultRendererPath,
|
||||||
renderOptions: {
|
renderOptions: {
|
||||||
authors: true,
|
authors: true,
|
||||||
mapAuthorsToGitHubUsernames: true,
|
applyUsernameToAuthors: true,
|
||||||
commitReferences: true,
|
commitReferences: true,
|
||||||
versionTitleDate: true,
|
versionTitleDate: true,
|
||||||
},
|
},
|
||||||
@ -1276,14 +1278,20 @@ const supportedCreateReleaseProviders = [
|
|||||||
name: 'github-enterprise-server',
|
name: 'github-enterprise-server',
|
||||||
defaultApiBaseUrl: 'https://__hostname__/api/v3',
|
defaultApiBaseUrl: 'https://__hostname__/api/v3',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'gitlab',
|
||||||
|
defaultApiBaseUrl: 'https://__hostname__/api/v4',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// User opts into the default by specifying the string value 'github'
|
/**
|
||||||
export const defaultCreateReleaseProvider = {
|
* Full form of the createRelease config, with the provider, hostname, and apiBaseUrl resolved.
|
||||||
provider: 'github',
|
*/
|
||||||
hostname: 'github.com',
|
export interface ResolvedCreateRemoteReleaseProvider {
|
||||||
apiBaseUrl: 'https://api.github.com',
|
provider: string;
|
||||||
} as any;
|
hostname: string;
|
||||||
|
apiBaseUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
function validateCreateReleaseConfig(
|
function validateCreateReleaseConfig(
|
||||||
changelogConfig: NxReleaseChangelogConfiguration
|
changelogConfig: NxReleaseChangelogConfiguration
|
||||||
@ -1295,7 +1303,14 @@ function validateCreateReleaseConfig(
|
|||||||
}
|
}
|
||||||
// GitHub shorthand, expand to full object form, mark as valid
|
// GitHub shorthand, expand to full object form, mark as valid
|
||||||
if (createRelease === 'github') {
|
if (createRelease === 'github') {
|
||||||
changelogConfig.createRelease = defaultCreateReleaseProvider;
|
changelogConfig.createRelease =
|
||||||
|
defaultGitHubCreateReleaseProvider as unknown as NxReleaseChangelogConfiguration['createRelease'];
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// Gitlab shorthand, expand to full object form, mark as valid
|
||||||
|
if (createRelease === 'gitlab') {
|
||||||
|
changelogConfig.createRelease =
|
||||||
|
defaultGitLabCreateReleaseProvider as unknown as NxReleaseChangelogConfiguration['createRelease'];
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
// Object config, ensure that properties are valid
|
// Object config, ensure that properties are valid
|
||||||
|
|||||||
@ -5,13 +5,12 @@ import { createProjectFileMapUsingProjectGraph } from '../../project-graph/file-
|
|||||||
import { createProjectGraphAsync } from '../../project-graph/project-graph';
|
import { createProjectGraphAsync } from '../../project-graph/project-graph';
|
||||||
import { handleErrors } from '../../utils/handle-errors';
|
import { handleErrors } from '../../utils/handle-errors';
|
||||||
import { output } from '../../utils/output';
|
import { output } from '../../utils/output';
|
||||||
import {
|
import { createAPI as createReleaseChangelogAPI } from './changelog';
|
||||||
createAPI as createReleaseChangelogAPI,
|
|
||||||
shouldCreateGitHubRelease,
|
|
||||||
} from './changelog';
|
|
||||||
import { ReleaseOptions, VersionOptions } from './command-object';
|
import { ReleaseOptions, VersionOptions } from './command-object';
|
||||||
import {
|
import {
|
||||||
IMPLICIT_DEFAULT_RELEASE_GROUP,
|
IMPLICIT_DEFAULT_RELEASE_GROUP,
|
||||||
|
NxReleaseConfig,
|
||||||
|
ResolvedCreateRemoteReleaseProvider,
|
||||||
createNxReleaseConfig,
|
createNxReleaseConfig,
|
||||||
handleNxReleaseConfigError,
|
handleNxReleaseConfigError,
|
||||||
} from './config/config';
|
} from './config/config';
|
||||||
@ -24,8 +23,8 @@ import {
|
|||||||
} from './config/version-plans';
|
} from './config/version-plans';
|
||||||
import { createAPI as createReleasePublishAPI } from './publish';
|
import { createAPI as createReleasePublishAPI } from './publish';
|
||||||
import { getCommitHash, gitAdd, gitCommit, gitPush, gitTag } from './utils/git';
|
import { getCommitHash, gitAdd, gitCommit, gitPush, gitTag } from './utils/git';
|
||||||
import { createOrUpdateGithubRelease } from './utils/github';
|
|
||||||
import { printConfigAndExit } from './utils/print-config';
|
import { printConfigAndExit } from './utils/print-config';
|
||||||
|
import { createRemoteReleaseClient } from './utils/remote-release-clients/remote-release-client';
|
||||||
import { resolveNxJsonConfigErrorMessage } from './utils/resolve-nx-json-error-message';
|
import { resolveNxJsonConfigErrorMessage } from './utils/resolve-nx-json-error-message';
|
||||||
import {
|
import {
|
||||||
createCommitMessageValues,
|
createCommitMessageValues,
|
||||||
@ -137,14 +136,14 @@ export function createAPI(overrideReleaseConfig: NxReleaseConfiguration) {
|
|||||||
(shouldCommit || userProvidedReleaseConfig.git?.stageChanges) ?? false;
|
(shouldCommit || userProvidedReleaseConfig.git?.stageChanges) ?? false;
|
||||||
const shouldTag = userProvidedReleaseConfig.git?.tag ?? true;
|
const shouldTag = userProvidedReleaseConfig.git?.tag ?? true;
|
||||||
|
|
||||||
const shouldCreateWorkspaceRelease = shouldCreateGitHubRelease(
|
const shouldCreateWorkspaceRemoteRelease = shouldCreateRemoteRelease(
|
||||||
nxReleaseConfig.changelog.workspaceChangelog
|
nxReleaseConfig.changelog.workspaceChangelog
|
||||||
);
|
);
|
||||||
// If the workspace or any of the release groups specify that a github release should be created, we need to push the changes to the remote
|
// If the workspace or any of the release groups specify that a remote release should be created, we need to push the changes to the remote
|
||||||
const shouldPush =
|
const shouldPush =
|
||||||
(shouldCreateWorkspaceRelease ||
|
(shouldCreateWorkspaceRemoteRelease ||
|
||||||
releaseGroups.some((group) =>
|
releaseGroups.some((group) =>
|
||||||
shouldCreateGitHubRelease(group.changelog)
|
shouldCreateRemoteRelease(group.changelog)
|
||||||
)) ??
|
)) ??
|
||||||
false;
|
false;
|
||||||
|
|
||||||
@ -268,20 +267,28 @@ export function createAPI(overrideReleaseConfig: NxReleaseConfiguration) {
|
|||||||
|
|
||||||
let latestCommit: string | undefined;
|
let latestCommit: string | undefined;
|
||||||
|
|
||||||
if (shouldCreateWorkspaceRelease && changelogResult.workspaceChangelog) {
|
if (
|
||||||
|
shouldCreateWorkspaceRemoteRelease &&
|
||||||
|
changelogResult.workspaceChangelog
|
||||||
|
) {
|
||||||
|
const remoteReleaseClient = await createRemoteReleaseClient(
|
||||||
|
// shouldCreateWorkspaceRemoteRelease() ensures that the createRelease property exists and is not false
|
||||||
|
(nxReleaseConfig.changelog.workspaceChangelog as any)
|
||||||
|
.createRelease as ResolvedCreateRemoteReleaseProvider
|
||||||
|
);
|
||||||
if (!hasPushedChanges) {
|
if (!hasPushedChanges) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'It is not possible to create a github release for the workspace without pushing the changes to the remote, please ensure that you have not disabled git push in your nx release config'
|
`It is not possible to create a ${remoteReleaseClient.remoteReleaseProviderName} release for the workspace without pushing the changes to the remote, please ensure that you have not disabled git push in your nx release config`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
output.logSingleLine(`Creating GitHub Release`);
|
output.logSingleLine(
|
||||||
|
`Creating ${remoteReleaseClient.remoteReleaseProviderName} Release`
|
||||||
|
);
|
||||||
|
|
||||||
latestCommit = await getCommitHash('HEAD');
|
latestCommit = await getCommitHash('HEAD');
|
||||||
await createOrUpdateGithubRelease(
|
|
||||||
nxReleaseConfig.changelog.workspaceChangelog
|
await remoteReleaseClient.createOrUpdateRelease(
|
||||||
? nxReleaseConfig.changelog.workspaceChangelog.createRelease
|
|
||||||
: false,
|
|
||||||
changelogResult.workspaceChangelog.releaseVersion,
|
changelogResult.workspaceChangelog.releaseVersion,
|
||||||
changelogResult.workspaceChangelog.contents,
|
changelogResult.workspaceChangelog.contents,
|
||||||
latestCommit,
|
latestCommit,
|
||||||
@ -290,11 +297,19 @@ export function createAPI(overrideReleaseConfig: NxReleaseConfiguration) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const releaseGroup of releaseGroups) {
|
for (const releaseGroup of releaseGroups) {
|
||||||
const shouldCreateProjectReleases = shouldCreateGitHubRelease(
|
const shouldCreateProjectRemoteReleases = shouldCreateRemoteRelease(
|
||||||
releaseGroup.changelog
|
releaseGroup.changelog
|
||||||
);
|
);
|
||||||
|
if (
|
||||||
|
shouldCreateProjectRemoteReleases &&
|
||||||
|
changelogResult.projectChangelogs
|
||||||
|
) {
|
||||||
|
const remoteReleaseClient = await createRemoteReleaseClient(
|
||||||
|
// shouldCreateProjectRemoteReleases() ensures that the createRelease property exists and is not false
|
||||||
|
(releaseGroup.changelog as any)
|
||||||
|
.createRelease as ResolvedCreateRemoteReleaseProvider
|
||||||
|
);
|
||||||
|
|
||||||
if (shouldCreateProjectReleases && changelogResult.projectChangelogs) {
|
|
||||||
const projects = args.projects?.length
|
const projects = args.projects?.length
|
||||||
? // If the user has passed a list of projects, we need to use the filtered list of projects within the release group
|
? // If the user has passed a list of projects, we need to use the filtered list of projects within the release group
|
||||||
Array.from(releaseGroupToFilteredProjects.get(releaseGroup))
|
Array.from(releaseGroupToFilteredProjects.get(releaseGroup))
|
||||||
@ -310,20 +325,19 @@ export function createAPI(overrideReleaseConfig: NxReleaseConfiguration) {
|
|||||||
|
|
||||||
if (!hasPushedChanges) {
|
if (!hasPushedChanges) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'It is not possible to create a github release for the project without pushing the changes to the remote, please ensure that you have not disabled git push in your nx release config'
|
`It is not possible to create a ${remoteReleaseClient.remoteReleaseProviderName} release for the project without pushing the changes to the remote, please ensure that you have not disabled git push in your nx release config`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
output.logSingleLine(`Creating GitHub Release`);
|
output.logSingleLine(
|
||||||
|
`Creating ${remoteReleaseClient.remoteReleaseProviderName} Release`
|
||||||
|
);
|
||||||
|
|
||||||
if (!latestCommit) {
|
if (!latestCommit) {
|
||||||
latestCommit = await getCommitHash('HEAD');
|
latestCommit = await getCommitHash('HEAD');
|
||||||
}
|
}
|
||||||
|
|
||||||
await createOrUpdateGithubRelease(
|
await remoteReleaseClient.createOrUpdateRelease(
|
||||||
releaseGroup.changelog
|
|
||||||
? releaseGroup.changelog.createRelease
|
|
||||||
: false,
|
|
||||||
changelog.releaseVersion,
|
changelog.releaseVersion,
|
||||||
changelog.contents,
|
changelog.contents,
|
||||||
latestCommit,
|
latestCommit,
|
||||||
@ -376,3 +390,15 @@ async function promptForPublish(): Promise<boolean> {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function shouldCreateRemoteRelease(
|
||||||
|
changelogConfig:
|
||||||
|
| NxReleaseConfig['changelog']['workspaceChangelog']
|
||||||
|
| NxReleaseConfig['changelog']['projectChangelogs']
|
||||||
|
| NxReleaseConfig['groups'][number]['changelog']
|
||||||
|
): boolean {
|
||||||
|
if (changelogConfig === false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return changelogConfig.createRelease !== false;
|
||||||
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { getLatestGitTagForPattern } from './git';
|
import { extractReferencesFromCommit, getLatestGitTagForPattern } from './git';
|
||||||
|
|
||||||
jest.mock('./exec-command', () => ({
|
jest.mock('./exec-command', () => ({
|
||||||
execCommand: jest.fn(() =>
|
execCommand: jest.fn(() =>
|
||||||
@ -20,104 +20,213 @@ my-lib-34.0.0-beta.1
|
|||||||
),
|
),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const releaseTagPatternTestCases = [
|
describe('git utils', () => {
|
||||||
{
|
describe('extractReferencesFromCommit', () => {
|
||||||
pattern: 'v{version}',
|
it('should include the given short commit hash even if no other references are found', () => {
|
||||||
projectName: 'my-lib-1',
|
const references = extractReferencesFromCommit({
|
||||||
expectedTag: 'v4.0.1',
|
message: 'test',
|
||||||
expectedVersion: '4.0.1',
|
body: '',
|
||||||
},
|
shortHash: 'abc123',
|
||||||
{
|
author: { name: 'Test Author', email: 'test@example.com' },
|
||||||
pattern: 'x{version}',
|
});
|
||||||
projectName: 'my-lib-1',
|
expect(references).toMatchInlineSnapshot(`
|
||||||
expectedTag: 'x5.0.0',
|
[
|
||||||
expectedVersion: '5.0.0',
|
{
|
||||||
},
|
"type": "hash",
|
||||||
{
|
"value": "abc123",
|
||||||
pattern: 'release/{version}',
|
},
|
||||||
projectName: 'my-lib-1',
|
]
|
||||||
expectedTag: 'release/4.2.1',
|
`);
|
||||||
expectedVersion: '4.2.1',
|
});
|
||||||
},
|
|
||||||
{
|
|
||||||
pattern: 'release/{projectName}@v{version}',
|
|
||||||
projectName: 'my-lib-1',
|
|
||||||
expectedTag: 'release/my-lib-1@v4.2.1',
|
|
||||||
expectedVersion: '4.2.1',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
pattern: '{version}',
|
|
||||||
projectName: 'my-lib-1',
|
|
||||||
expectedTag: '4.0.0-rc.1+build.1',
|
|
||||||
expectedVersion: '4.0.0-rc.1+build.1',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
pattern: '{projectName}@v{version}',
|
|
||||||
projectName: 'my-lib-1',
|
|
||||||
expectedTag: 'my-lib-1@v4.0.0-beta.1',
|
|
||||||
expectedVersion: '4.0.0-beta.1',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
pattern: '{projectName}v{version}',
|
|
||||||
projectName: 'my-lib-2',
|
|
||||||
expectedTag: 'my-lib-2v4.0.0-beta.1',
|
|
||||||
expectedVersion: '4.0.0-beta.1',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
pattern: '{projectName}{version}',
|
|
||||||
projectName: 'my-lib-3',
|
|
||||||
expectedTag: 'my-lib-34.0.0-beta.1',
|
|
||||||
expectedVersion: '4.0.0-beta.1',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
pattern: '{version}-{projectName}',
|
|
||||||
projectName: 'my-lib-1',
|
|
||||||
expectedTag: '4.0.0-beta.0-my-lib-1',
|
|
||||||
expectedVersion: '4.0.0-beta.0',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
pattern: '{version}-{projectName}',
|
|
||||||
projectName: 'alpha',
|
|
||||||
expectedTag: '3.0.0-beta.0-alpha',
|
|
||||||
expectedVersion: '3.0.0-beta.0',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
describe('getLatestGitTagForPattern', () => {
|
it('should match GitHub style issue references', () => {
|
||||||
afterEach(() => {
|
const references = extractReferencesFromCommit({
|
||||||
jest.clearAllMocks();
|
message: 'This fixed issue #789',
|
||||||
|
body: '',
|
||||||
|
shortHash: 'abc123',
|
||||||
|
author: { name: 'Test Author', email: 'test@example.com' },
|
||||||
|
});
|
||||||
|
expect(references).toMatchInlineSnapshot(`
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"type": "issue",
|
||||||
|
"value": "#789",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "hash",
|
||||||
|
"value": "abc123",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should match GitHub style PR references', () => {
|
||||||
|
const references = extractReferencesFromCommit({
|
||||||
|
message: 'fix: all the things (#20607)',
|
||||||
|
body: '',
|
||||||
|
shortHash: 'abc123',
|
||||||
|
author: { name: 'Test Author', email: 'test@example.com' },
|
||||||
|
});
|
||||||
|
expect(references).toMatchInlineSnapshot(`
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"type": "pull-request",
|
||||||
|
"value": "#20607",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "hash",
|
||||||
|
"value": "abc123",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should match GitLab style issue references', () => {
|
||||||
|
const references = extractReferencesFromCommit({
|
||||||
|
message: "Merge branch 'mr-to-fix-issue' into 'main'",
|
||||||
|
body: `fix: this should resolve the gitlab issue
|
||||||
|
|
||||||
|
Closes #1`,
|
||||||
|
shortHash: 'abc123',
|
||||||
|
author: { name: 'Test Author', email: 'test@example.com' },
|
||||||
|
});
|
||||||
|
expect(references).toMatchInlineSnapshot(`
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"type": "issue",
|
||||||
|
"value": "#1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "hash",
|
||||||
|
"value": "abc123",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should match GitLab style merge request references', () => {
|
||||||
|
const references = extractReferencesFromCommit({
|
||||||
|
message: "Merge branch 'mr-to-fix-issue' into 'main'",
|
||||||
|
body: `fix: this should resolve the gitlab issue
|
||||||
|
|
||||||
|
See merge request nx-release-test/nx-release-test!2`,
|
||||||
|
shortHash: 'abc123',
|
||||||
|
author: { name: 'Test Author', email: 'test@example.com' },
|
||||||
|
});
|
||||||
|
expect(references).toMatchInlineSnapshot(`
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"type": "pull-request",
|
||||||
|
"value": "!2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "hash",
|
||||||
|
"value": "abc123",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
`);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each(releaseTagPatternTestCases)(
|
const releaseTagPatternTestCases = [
|
||||||
'should return tag $expectedTag for pattern $pattern',
|
{
|
||||||
async ({ pattern, projectName, expectedTag, expectedVersion }) => {
|
pattern: 'v{version}',
|
||||||
const result = await getLatestGitTagForPattern(pattern, {
|
projectName: 'my-lib-1',
|
||||||
projectName,
|
expectedTag: 'v4.0.1',
|
||||||
});
|
expectedVersion: '4.0.1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: 'x{version}',
|
||||||
|
projectName: 'my-lib-1',
|
||||||
|
expectedTag: 'x5.0.0',
|
||||||
|
expectedVersion: '5.0.0',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: 'release/{version}',
|
||||||
|
projectName: 'my-lib-1',
|
||||||
|
expectedTag: 'release/4.2.1',
|
||||||
|
expectedVersion: '4.2.1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: 'release/{projectName}@v{version}',
|
||||||
|
projectName: 'my-lib-1',
|
||||||
|
expectedTag: 'release/my-lib-1@v4.2.1',
|
||||||
|
expectedVersion: '4.2.1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: '{version}',
|
||||||
|
projectName: 'my-lib-1',
|
||||||
|
expectedTag: '4.0.0-rc.1+build.1',
|
||||||
|
expectedVersion: '4.0.0-rc.1+build.1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: '{projectName}@v{version}',
|
||||||
|
projectName: 'my-lib-1',
|
||||||
|
expectedTag: 'my-lib-1@v4.0.0-beta.1',
|
||||||
|
expectedVersion: '4.0.0-beta.1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: '{projectName}v{version}',
|
||||||
|
projectName: 'my-lib-2',
|
||||||
|
expectedTag: 'my-lib-2v4.0.0-beta.1',
|
||||||
|
expectedVersion: '4.0.0-beta.1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: '{projectName}{version}',
|
||||||
|
projectName: 'my-lib-3',
|
||||||
|
expectedTag: 'my-lib-34.0.0-beta.1',
|
||||||
|
expectedVersion: '4.0.0-beta.1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: '{version}-{projectName}',
|
||||||
|
projectName: 'my-lib-1',
|
||||||
|
expectedTag: '4.0.0-beta.0-my-lib-1',
|
||||||
|
expectedVersion: '4.0.0-beta.0',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: '{version}-{projectName}',
|
||||||
|
projectName: 'alpha',
|
||||||
|
expectedTag: '3.0.0-beta.0-alpha',
|
||||||
|
expectedVersion: '3.0.0-beta.0',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
expect(result.tag).toEqual(expectedTag);
|
describe('getLatestGitTagForPattern', () => {
|
||||||
expect(result.extractedVersion).toEqual(expectedVersion);
|
afterEach(() => {
|
||||||
}
|
jest.clearAllMocks();
|
||||||
);
|
});
|
||||||
|
|
||||||
it('should return null if execCommand throws an error', async () => {
|
it.each(releaseTagPatternTestCases)(
|
||||||
// should return null if execCommand throws an error
|
'should return tag $expectedTag for pattern $pattern',
|
||||||
(require('./exec-command').execCommand as jest.Mock).mockImplementationOnce(
|
async ({ pattern, projectName, expectedTag, expectedVersion }) => {
|
||||||
() => {
|
const result = await getLatestGitTagForPattern(pattern, {
|
||||||
throw new Error('error');
|
projectName,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.tag).toEqual(expectedTag);
|
||||||
|
expect(result.extractedVersion).toEqual(expectedVersion);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
const result = await getLatestGitTagForPattern('#{version}', {
|
|
||||||
projectName: 'my-lib-1',
|
|
||||||
});
|
|
||||||
expect(result).toEqual(null);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return null if no tags match the pattern', async () => {
|
it('should return null if execCommand throws an error', async () => {
|
||||||
const result = await getLatestGitTagForPattern('#{version}', {
|
// should return null if execCommand throws an error
|
||||||
projectName: 'my-lib-1',
|
(
|
||||||
|
require('./exec-command').execCommand as jest.Mock
|
||||||
|
).mockImplementationOnce(() => {
|
||||||
|
throw new Error('error');
|
||||||
|
});
|
||||||
|
const result = await getLatestGitTagForPattern('#{version}', {
|
||||||
|
projectName: 'my-lib-1',
|
||||||
|
});
|
||||||
|
expect(result).toEqual(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result).toEqual(null);
|
it('should return null if no tags match the pattern', async () => {
|
||||||
|
const result = await getLatestGitTagForPattern('#{version}', {
|
||||||
|
projectName: 'my-lib-1',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual(null);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -488,20 +488,38 @@ export function parseConventionalCommitsMessage(message: string): {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractReferencesFromCommitMessage(
|
export function extractReferencesFromCommit(commit: RawGitCommit): Reference[] {
|
||||||
message: string,
|
|
||||||
shortHash: string
|
|
||||||
): Reference[] {
|
|
||||||
const references: Reference[] = [];
|
const references: Reference[] = [];
|
||||||
for (const m of message.matchAll(PullRequestRE)) {
|
|
||||||
|
// Extract GitHub style PR references from commit message
|
||||||
|
for (const m of commit.message.matchAll(PullRequestRE)) {
|
||||||
references.push({ type: 'pull-request', value: m[1] });
|
references.push({ type: 'pull-request', value: m[1] });
|
||||||
}
|
}
|
||||||
for (const m of message.matchAll(IssueRE)) {
|
|
||||||
|
// Extract GitLab style merge request references from commit body
|
||||||
|
for (const m of commit.body.matchAll(GitLabMergeRequestRE)) {
|
||||||
|
if (m[1]) {
|
||||||
|
references.push({ type: 'pull-request', value: m[1] });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract issue references from commit message
|
||||||
|
for (const m of commit.message.matchAll(IssueRE)) {
|
||||||
if (!references.some((i) => i.value === m[1])) {
|
if (!references.some((i) => i.value === m[1])) {
|
||||||
references.push({ type: 'issue', value: m[1] });
|
references.push({ type: 'issue', value: m[1] });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
references.push({ value: shortHash, type: 'hash' });
|
|
||||||
|
// Extract issue references from commit body
|
||||||
|
for (const m of commit.body.matchAll(IssueRE)) {
|
||||||
|
if (!references.some((i) => i.value === m[1])) {
|
||||||
|
references.push({ type: 'issue', value: m[1] });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add commit hash reference
|
||||||
|
references.push({ value: commit.shortHash, type: 'hash' });
|
||||||
|
|
||||||
return references;
|
return references;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -522,7 +540,10 @@ function getAllAuthorsForCommit(commit: RawGitCommit): GitCommitAuthor[] {
|
|||||||
const ConventionalCommitRegex =
|
const ConventionalCommitRegex =
|
||||||
/(?<type>[a-z]+)(\((?<scope>.+)\))?(?<breaking>!)?: (?<description>.+)/i;
|
/(?<type>[a-z]+)(\((?<scope>.+)\))?(?<breaking>!)?: (?<description>.+)/i;
|
||||||
const CoAuthoredByRegex = /co-authored-by:\s*(?<name>.+)(<(?<email>.+)>)/gim;
|
const CoAuthoredByRegex = /co-authored-by:\s*(?<name>.+)(<(?<email>.+)>)/gim;
|
||||||
|
// GitHub style PR references
|
||||||
const PullRequestRE = /\([ a-z]*(#\d+)\s*\)/gm;
|
const PullRequestRE = /\([ a-z]*(#\d+)\s*\)/gm;
|
||||||
|
// GitLab style merge request references
|
||||||
|
const GitLabMergeRequestRE = /See merge request (?:[a-z0-9/-]+)?(![\d]+)/gim;
|
||||||
const IssueRE = /(#\d+)/gm;
|
const IssueRE = /(#\d+)/gm;
|
||||||
const ChangedFileRegex = /(A|M|D|R\d*|C\d*)\t([^\t\n]*)\t?(.*)?/gm;
|
const ChangedFileRegex = /(A|M|D|R\d*|C\d*)\t([^\t\n]*)\t?(.*)?/gm;
|
||||||
const RevertHashRE = /This reverts commit (?<hash>[\da-f]{40})./gm;
|
const RevertHashRE = /This reverts commit (?<hash>[\da-f]{40})./gm;
|
||||||
@ -538,10 +559,7 @@ export function parseGitCommit(
|
|||||||
description: commit.message,
|
description: commit.message,
|
||||||
type: '',
|
type: '',
|
||||||
scope: '',
|
scope: '',
|
||||||
references: extractReferencesFromCommitMessage(
|
references: extractReferencesFromCommit(commit),
|
||||||
commit.message,
|
|
||||||
commit.shortHash
|
|
||||||
),
|
|
||||||
// The commit message is not the source of truth for a breaking (major) change in version plans, so the value is not relevant
|
// The commit message is not the source of truth for a breaking (major) change in version plans, so the value is not relevant
|
||||||
// TODO(v22): Make the current GitCommit interface more clearly tied to conventional commits
|
// TODO(v22): Make the current GitCommit interface more clearly tied to conventional commits
|
||||||
isBreaking: false,
|
isBreaking: false,
|
||||||
@ -563,13 +581,10 @@ export function parseGitCommit(
|
|||||||
parsedMessage.breaking || commit.body.includes('BREAKING CHANGE:');
|
parsedMessage.breaking || commit.body.includes('BREAKING CHANGE:');
|
||||||
let description = parsedMessage.description;
|
let description = parsedMessage.description;
|
||||||
|
|
||||||
// Extract references from message
|
// Extract issue and PR references from the commit
|
||||||
const references = extractReferencesFromCommitMessage(
|
const references = extractReferencesFromCommit(commit);
|
||||||
description,
|
|
||||||
commit.shortHash
|
|
||||||
);
|
|
||||||
|
|
||||||
// Remove references and normalize
|
// Remove GitHub style references from description (NOTE: GitLab style references only seem to appear in the body, so we don't need to remove them here)
|
||||||
description = description.replace(PullRequestRE, '').trim();
|
description = description.replace(PullRequestRE, '').trim();
|
||||||
|
|
||||||
let type = parsedMessage.type;
|
let type = parsedMessage.type;
|
||||||
|
|||||||
@ -1,493 +0,0 @@
|
|||||||
/**
|
|
||||||
* Special thanks to changelogen for the original inspiration for many of these utilities:
|
|
||||||
* https://github.com/unjs/changelogen
|
|
||||||
*/
|
|
||||||
import type { AxiosRequestConfig } from 'axios';
|
|
||||||
import * as chalk from 'chalk';
|
|
||||||
import { prompt } from 'enquirer';
|
|
||||||
import { execSync } from 'node:child_process';
|
|
||||||
import { existsSync, promises as fsp } from 'node:fs';
|
|
||||||
import { homedir } from 'node:os';
|
|
||||||
import { NxReleaseChangelogConfiguration } from '../../../config/nx-json';
|
|
||||||
import { output } from '../../../utils/output';
|
|
||||||
import { joinPathFragments } from '../../../utils/path';
|
|
||||||
import { defaultCreateReleaseProvider } from '../config/config';
|
|
||||||
import { Reference } from './git';
|
|
||||||
import { printDiff } from './print-changes';
|
|
||||||
import { ReleaseVersion, noDiffInChangelogMessage } from './shared';
|
|
||||||
|
|
||||||
// axios types and values don't seem to match
|
|
||||||
import _axios = require('axios');
|
|
||||||
const axios = _axios as any as (typeof _axios)['default'];
|
|
||||||
|
|
||||||
export type RepoSlug = `${string}/${string}`;
|
|
||||||
|
|
||||||
interface GithubRequestConfig {
|
|
||||||
repo: string;
|
|
||||||
hostname: string;
|
|
||||||
apiBaseUrl: string;
|
|
||||||
token: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://docs.github.com/en/rest/releases/releases?apiVersion=2022-11-28#create-a-release--parameters
|
|
||||||
interface GithubRelease {
|
|
||||||
id?: string;
|
|
||||||
tag_name: string;
|
|
||||||
target_commitish?: string;
|
|
||||||
name?: string;
|
|
||||||
body?: string;
|
|
||||||
draft?: boolean;
|
|
||||||
prerelease?: boolean;
|
|
||||||
make_latest?: 'legacy' | boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GithubRepoData {
|
|
||||||
hostname: string;
|
|
||||||
slug: RepoSlug;
|
|
||||||
apiBaseUrl: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getGitHubRepoData(
|
|
||||||
remoteName = 'origin',
|
|
||||||
createReleaseConfig: NxReleaseChangelogConfiguration['createRelease']
|
|
||||||
): GithubRepoData | null {
|
|
||||||
try {
|
|
||||||
const remoteUrl = execSync(`git remote get-url ${remoteName}`, {
|
|
||||||
encoding: 'utf8',
|
|
||||||
stdio: 'pipe',
|
|
||||||
}).trim();
|
|
||||||
|
|
||||||
// Use the default provider (github.com) if custom one is not specified or releases are disabled
|
|
||||||
let hostname = defaultCreateReleaseProvider.hostname;
|
|
||||||
let apiBaseUrl = defaultCreateReleaseProvider.apiBaseUrl;
|
|
||||||
if (
|
|
||||||
createReleaseConfig !== false &&
|
|
||||||
typeof createReleaseConfig !== 'string'
|
|
||||||
) {
|
|
||||||
hostname = createReleaseConfig.hostname;
|
|
||||||
apiBaseUrl = createReleaseConfig.apiBaseUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract the 'user/repo' part from the URL
|
|
||||||
const escapedHostname = hostname.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
||||||
const regexString = `${escapedHostname}[/:]([\\w.-]+/[\\w.-]+)(\\.git)?`;
|
|
||||||
const regex = new RegExp(regexString);
|
|
||||||
const match = remoteUrl.match(regex);
|
|
||||||
|
|
||||||
if (match && match[1]) {
|
|
||||||
return {
|
|
||||||
hostname,
|
|
||||||
apiBaseUrl,
|
|
||||||
// Ensure any trailing .git is stripped
|
|
||||||
slug: match[1].replace(/\.git$/, '') as RepoSlug,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
throw new Error(
|
|
||||||
`Could not extract "user/repo" data from the resolved remote URL: ${remoteUrl}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createOrUpdateGithubRelease(
|
|
||||||
createReleaseConfig: NxReleaseChangelogConfiguration['createRelease'],
|
|
||||||
releaseVersion: ReleaseVersion,
|
|
||||||
changelogContents: string,
|
|
||||||
latestCommit: string,
|
|
||||||
{ dryRun }: { dryRun: boolean }
|
|
||||||
): Promise<void> {
|
|
||||||
const githubRepoData = getGitHubRepoData(undefined, createReleaseConfig);
|
|
||||||
if (!githubRepoData) {
|
|
||||||
output.error({
|
|
||||||
title: `Unable to create a GitHub release because the GitHub repo slug could not be determined.`,
|
|
||||||
bodyLines: [
|
|
||||||
`Please ensure you have a valid GitHub remote configured. You can run \`git remote -v\` to list your current remotes.`,
|
|
||||||
],
|
|
||||||
});
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const token = await resolveGithubToken(githubRepoData.hostname);
|
|
||||||
const githubRequestConfig: GithubRequestConfig = {
|
|
||||||
repo: githubRepoData.slug,
|
|
||||||
hostname: githubRepoData.hostname,
|
|
||||||
apiBaseUrl: githubRepoData.apiBaseUrl,
|
|
||||||
token,
|
|
||||||
};
|
|
||||||
|
|
||||||
let existingGithubReleaseForVersion: GithubRelease;
|
|
||||||
try {
|
|
||||||
existingGithubReleaseForVersion = await getGithubReleaseByTag(
|
|
||||||
githubRequestConfig,
|
|
||||||
releaseVersion.gitTag
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
if (err.response?.status === 401) {
|
|
||||||
output.error({
|
|
||||||
title: `Unable to resolve data via the GitHub API. You can use any of the following options to resolve this:`,
|
|
||||||
bodyLines: [
|
|
||||||
'- Set the `GITHUB_TOKEN` or `GH_TOKEN` environment variable to a valid GitHub token with `repo` scope',
|
|
||||||
'- Have an active session via the official gh CLI tool (https://cli.github.com) in your current terminal',
|
|
||||||
],
|
|
||||||
});
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
if (err.response?.status === 404) {
|
|
||||||
// No existing release found, this is fine
|
|
||||||
} else {
|
|
||||||
// Rethrow unknown errors for now
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const logTitle = `https://${githubRepoData.hostname}/${githubRepoData.slug}/releases/tag/${releaseVersion.gitTag}`;
|
|
||||||
if (existingGithubReleaseForVersion) {
|
|
||||||
console.error(
|
|
||||||
`${chalk.white('UPDATE')} ${logTitle}${
|
|
||||||
dryRun ? chalk.keyword('orange')(' [dry-run]') : ''
|
|
||||||
}`
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
console.error(
|
|
||||||
`${chalk.green('CREATE')} ${logTitle}${
|
|
||||||
dryRun ? chalk.keyword('orange')(' [dry-run]') : ''
|
|
||||||
}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('');
|
|
||||||
printDiff(
|
|
||||||
existingGithubReleaseForVersion ? existingGithubReleaseForVersion.body : '',
|
|
||||||
changelogContents,
|
|
||||||
3,
|
|
||||||
noDiffInChangelogMessage
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!dryRun) {
|
|
||||||
await createOrUpdateGithubReleaseInternal(
|
|
||||||
githubRequestConfig,
|
|
||||||
{
|
|
||||||
version: releaseVersion.gitTag,
|
|
||||||
prerelease: releaseVersion.isPrerelease,
|
|
||||||
body: changelogContents,
|
|
||||||
commit: latestCommit,
|
|
||||||
},
|
|
||||||
existingGithubReleaseForVersion
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface GithubReleaseOptions {
|
|
||||||
version: string;
|
|
||||||
body: string;
|
|
||||||
prerelease: boolean;
|
|
||||||
commit: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createOrUpdateGithubReleaseInternal(
|
|
||||||
githubRequestConfig: GithubRequestConfig,
|
|
||||||
release: GithubReleaseOptions,
|
|
||||||
existingGithubReleaseForVersion?: GithubRelease
|
|
||||||
) {
|
|
||||||
const result = await syncGithubRelease(
|
|
||||||
githubRequestConfig,
|
|
||||||
release,
|
|
||||||
existingGithubReleaseForVersion
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If something went wrong POSTing to Github we can still pre-populate the web form on github.com
|
|
||||||
* to allow the user to manually complete the release if they so choose.
|
|
||||||
*/
|
|
||||||
if (result.status === 'manual') {
|
|
||||||
if (result.error) {
|
|
||||||
process.exitCode = 1;
|
|
||||||
|
|
||||||
if (result.error.response?.data) {
|
|
||||||
// There's a nicely formatted error from GitHub we can display to the user
|
|
||||||
output.error({
|
|
||||||
title: `A GitHub API Error occurred when creating/updating the release`,
|
|
||||||
bodyLines: [
|
|
||||||
`GitHub Error: ${JSON.stringify(result.error.response.data)}`,
|
|
||||||
`---`,
|
|
||||||
`Request Data:`,
|
|
||||||
`Repo: ${githubRequestConfig.repo}`,
|
|
||||||
`Token: ${githubRequestConfig.token}`,
|
|
||||||
`Body: ${JSON.stringify(result.requestData)}`,
|
|
||||||
],
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
console.log(result.error);
|
|
||||||
console.error(
|
|
||||||
`An unknown error occurred while trying to create a release on GitHub, please report this on https://github.com/nrwl/nx (NOTE: make sure to redact your GitHub token from the error message!)`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const shouldContinueInGitHub = await promptForContinueInGitHub();
|
|
||||||
if (!shouldContinueInGitHub) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const open = require('open');
|
|
||||||
await open(result.url)
|
|
||||||
.then(() => {
|
|
||||||
console.info(
|
|
||||||
`\nFollow up in the browser to manually create the release:\n\n` +
|
|
||||||
chalk.underline(chalk.cyan(result.url)) +
|
|
||||||
`\n`
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
console.info(
|
|
||||||
`Open this link to manually create a release: \n` +
|
|
||||||
chalk.underline(chalk.cyan(result.url)) +
|
|
||||||
'\n'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If something went wrong POSTing to Github we can still pre-populate the web form on github.com
|
|
||||||
* to allow the user to manually complete the release.
|
|
||||||
*/
|
|
||||||
if (result.status === 'manual') {
|
|
||||||
if (result.error) {
|
|
||||||
console.error(result.error);
|
|
||||||
process.exitCode = 1;
|
|
||||||
}
|
|
||||||
const open = require('open');
|
|
||||||
await open(result.url)
|
|
||||||
.then(() => {
|
|
||||||
console.info(
|
|
||||||
`Follow up in the browser to manually create the release.`
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
console.info(
|
|
||||||
`Open this link to manually create a release: \n` +
|
|
||||||
chalk.underline(chalk.cyan(result.url)) +
|
|
||||||
'\n'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function promptForContinueInGitHub(): Promise<boolean> {
|
|
||||||
try {
|
|
||||||
const reply = await prompt<{ open: 'Yes' | 'No' }>([
|
|
||||||
{
|
|
||||||
name: 'open',
|
|
||||||
message:
|
|
||||||
'Do you want to finish creating the release manually in your browser?',
|
|
||||||
type: 'autocomplete',
|
|
||||||
choices: [
|
|
||||||
{
|
|
||||||
name: 'Yes',
|
|
||||||
hint: 'It will pre-populate the form for you',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'No',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
initial: 0,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
return reply.open === 'Yes';
|
|
||||||
} catch {
|
|
||||||
// Ensure the cursor is always restored before exiting
|
|
||||||
process.stdout.write('\u001b[?25h');
|
|
||||||
// Handle the case where the user exits the prompt with ctrl+c
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function syncGithubRelease(
|
|
||||||
githubRequestConfig: GithubRequestConfig,
|
|
||||||
release: GithubReleaseOptions,
|
|
||||||
existingGithubReleaseForVersion?: GithubRelease
|
|
||||||
) {
|
|
||||||
const ghRelease: GithubRelease = {
|
|
||||||
tag_name: release.version,
|
|
||||||
name: release.version,
|
|
||||||
body: release.body,
|
|
||||||
prerelease: release.prerelease,
|
|
||||||
// legacy specifies that the latest release should be determined based on the release creation date and higher semantic version.
|
|
||||||
make_latest: 'legacy',
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
const newGhRelease = await (existingGithubReleaseForVersion
|
|
||||||
? updateGithubRelease(
|
|
||||||
githubRequestConfig,
|
|
||||||
existingGithubReleaseForVersion.id,
|
|
||||||
ghRelease
|
|
||||||
)
|
|
||||||
: createGithubRelease(githubRequestConfig, {
|
|
||||||
...ghRelease,
|
|
||||||
target_commitish: release.commit,
|
|
||||||
}));
|
|
||||||
return {
|
|
||||||
status: existingGithubReleaseForVersion ? 'updated' : 'created',
|
|
||||||
id: newGhRelease.id,
|
|
||||||
url: newGhRelease.html_url,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return {
|
|
||||||
status: 'manual',
|
|
||||||
error,
|
|
||||||
url: githubNewReleaseURL(githubRequestConfig, release),
|
|
||||||
requestData: ghRelease,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function resolveGithubToken(hostname: string): Promise<string | null> {
|
|
||||||
// Try and resolve from the environment
|
|
||||||
const tokenFromEnv = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
|
|
||||||
if (tokenFromEnv) {
|
|
||||||
return tokenFromEnv;
|
|
||||||
}
|
|
||||||
// Try and resolve from gh CLI installation
|
|
||||||
const ghCLIPath = joinPathFragments(
|
|
||||||
process.env.XDG_CONFIG_HOME || joinPathFragments(homedir(), '.config'),
|
|
||||||
'gh',
|
|
||||||
'hosts.yml'
|
|
||||||
);
|
|
||||||
if (existsSync(ghCLIPath)) {
|
|
||||||
const yamlContents = await fsp.readFile(ghCLIPath, 'utf8');
|
|
||||||
const { load } = require('@zkochan/js-yaml');
|
|
||||||
const ghCLIConfig = load(yamlContents);
|
|
||||||
if (ghCLIConfig[hostname]) {
|
|
||||||
// Web based session (the token is already embedded in the config)
|
|
||||||
if (ghCLIConfig[hostname].oauth_token) {
|
|
||||||
return ghCLIConfig[hostname].oauth_token;
|
|
||||||
}
|
|
||||||
// SSH based session (we need to dynamically resolve a token using the CLI)
|
|
||||||
if (
|
|
||||||
ghCLIConfig[hostname].user &&
|
|
||||||
ghCLIConfig[hostname].git_protocol === 'ssh'
|
|
||||||
) {
|
|
||||||
return execSync(`gh auth token`, {
|
|
||||||
encoding: 'utf8',
|
|
||||||
stdio: 'pipe',
|
|
||||||
windowsHide: false,
|
|
||||||
}).trim();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (hostname !== 'github.com') {
|
|
||||||
console.log(
|
|
||||||
`Warning: It was not possible to automatically resolve a GitHub token from your environment for hostname ${hostname}. If you set the GITHUB_TOKEN or GH_TOKEN environment variable, that will be used for GitHub API requests.`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getGithubReleaseByTag(
|
|
||||||
config: GithubRequestConfig,
|
|
||||||
tag: string
|
|
||||||
): Promise<GithubRelease> {
|
|
||||||
return await makeGithubRequest(
|
|
||||||
config,
|
|
||||||
`/repos/${config.repo}/releases/tags/${tag}`,
|
|
||||||
{}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function makeGithubRequest(
|
|
||||||
config: GithubRequestConfig,
|
|
||||||
url: string,
|
|
||||||
opts: AxiosRequestConfig = {}
|
|
||||||
) {
|
|
||||||
return (
|
|
||||||
await axios<any, any>(url, {
|
|
||||||
...opts,
|
|
||||||
baseURL: config.apiBaseUrl,
|
|
||||||
headers: {
|
|
||||||
...(opts.headers as any),
|
|
||||||
Authorization: config.token ? `Bearer ${config.token}` : undefined,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
).data;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createGithubRelease(
|
|
||||||
config: GithubRequestConfig,
|
|
||||||
body: GithubRelease
|
|
||||||
) {
|
|
||||||
return await makeGithubRequest(config, `/repos/${config.repo}/releases`, {
|
|
||||||
method: 'POST',
|
|
||||||
data: body,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function updateGithubRelease(
|
|
||||||
config: GithubRequestConfig,
|
|
||||||
id: string,
|
|
||||||
body: GithubRelease
|
|
||||||
) {
|
|
||||||
return await makeGithubRequest(
|
|
||||||
config,
|
|
||||||
`/repos/${config.repo}/releases/${id}`,
|
|
||||||
{
|
|
||||||
method: 'PATCH',
|
|
||||||
data: body,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function githubNewReleaseURL(
|
|
||||||
config: GithubRequestConfig,
|
|
||||||
release: GithubReleaseOptions
|
|
||||||
) {
|
|
||||||
// Parameters taken from https://github.com/isaacs/github/issues/1410#issuecomment-442240267
|
|
||||||
let url = `https://${config.hostname}/${config.repo}/releases/new?tag=${
|
|
||||||
release.version
|
|
||||||
}&title=${release.version}&body=${encodeURIComponent(release.body)}&target=${
|
|
||||||
release.commit
|
|
||||||
}`;
|
|
||||||
if (release.prerelease) {
|
|
||||||
url += '&prerelease=true';
|
|
||||||
}
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
|
|
||||||
type RepoProvider = 'github';
|
|
||||||
|
|
||||||
const providerToRefSpec: Record<
|
|
||||||
RepoProvider,
|
|
||||||
Record<Reference['type'], string>
|
|
||||||
> = {
|
|
||||||
github: { 'pull-request': 'pull', hash: 'commit', issue: 'issues' },
|
|
||||||
};
|
|
||||||
|
|
||||||
function formatReference(ref: Reference, repoData: GithubRepoData) {
|
|
||||||
const refSpec = providerToRefSpec['github'];
|
|
||||||
return `[${ref.value}](https://${repoData.hostname}/${repoData.slug}/${
|
|
||||||
refSpec[ref.type]
|
|
||||||
}/${ref.value.replace(/^#/, '')})`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function formatReferences(
|
|
||||||
references: Reference[],
|
|
||||||
repoData: GithubRepoData
|
|
||||||
) {
|
|
||||||
const pr = references.filter((ref) => ref.type === 'pull-request');
|
|
||||||
const issue = references.filter((ref) => ref.type === 'issue');
|
|
||||||
if (pr.length > 0 || issue.length > 0) {
|
|
||||||
return (
|
|
||||||
' (' +
|
|
||||||
[...pr, ...issue]
|
|
||||||
.map((ref) => formatReference(ref, repoData))
|
|
||||||
.join(', ') +
|
|
||||||
')'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (references.length > 0) {
|
|
||||||
return ' (' + formatReference(references[0], repoData) + ')';
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
@ -0,0 +1,439 @@
|
|||||||
|
import * as chalk from 'chalk';
|
||||||
|
import { prompt } from 'enquirer';
|
||||||
|
import { execSync } from 'node:child_process';
|
||||||
|
import { existsSync, promises as fsp } from 'node:fs';
|
||||||
|
import { homedir } from 'node:os';
|
||||||
|
import { output } from '../../../../utils/output';
|
||||||
|
import { joinPathFragments } from '../../../../utils/path';
|
||||||
|
import type { PostGitTask } from '../../changelog';
|
||||||
|
import { type ResolvedCreateRemoteReleaseProvider } from '../../config/config';
|
||||||
|
import { Reference } from '../git';
|
||||||
|
import { ReleaseVersion } from '../shared';
|
||||||
|
import {
|
||||||
|
RemoteReleaseClient,
|
||||||
|
RemoteReleaseOptions,
|
||||||
|
RemoteReleaseResult,
|
||||||
|
RemoteRepoData,
|
||||||
|
RemoteRepoSlug,
|
||||||
|
} from './remote-release-client';
|
||||||
|
|
||||||
|
// axios types and values don't seem to match
|
||||||
|
import _axios = require('axios');
|
||||||
|
const axios = _axios as any as (typeof _axios)['default'];
|
||||||
|
|
||||||
|
export interface GithubRepoData extends RemoteRepoData {}
|
||||||
|
|
||||||
|
// https://docs.github.com/en/rest/releases/releases?apiVersion=2022-11-28#create-a-release--parameters
|
||||||
|
export interface GithubRemoteRelease {
|
||||||
|
id?: string;
|
||||||
|
body: string;
|
||||||
|
tag_name: string;
|
||||||
|
target_commitish?: string;
|
||||||
|
name?: string;
|
||||||
|
draft?: boolean;
|
||||||
|
prerelease?: boolean;
|
||||||
|
make_latest?: 'legacy' | boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaultCreateReleaseProvider: ResolvedCreateRemoteReleaseProvider =
|
||||||
|
{
|
||||||
|
provider: 'github',
|
||||||
|
hostname: 'github.com',
|
||||||
|
apiBaseUrl: 'https://api.github.com',
|
||||||
|
};
|
||||||
|
|
||||||
|
export class GithubRemoteReleaseClient extends RemoteReleaseClient<GithubRemoteRelease> {
|
||||||
|
remoteReleaseProviderName = 'GitHub';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get GitHub repository data from git remote
|
||||||
|
*/
|
||||||
|
static resolveRepoData(
|
||||||
|
createReleaseConfig: false | ResolvedCreateRemoteReleaseProvider,
|
||||||
|
remoteName = 'origin'
|
||||||
|
): GithubRepoData | null {
|
||||||
|
try {
|
||||||
|
const remoteUrl = execSync(`git remote get-url ${remoteName}`, {
|
||||||
|
encoding: 'utf8',
|
||||||
|
stdio: 'pipe',
|
||||||
|
}).trim();
|
||||||
|
|
||||||
|
// Use the default provider if custom one is not specified or releases are disabled
|
||||||
|
let hostname = defaultCreateReleaseProvider.hostname;
|
||||||
|
let apiBaseUrl = defaultCreateReleaseProvider.apiBaseUrl;
|
||||||
|
if (
|
||||||
|
createReleaseConfig !== false &&
|
||||||
|
typeof createReleaseConfig !== 'string'
|
||||||
|
) {
|
||||||
|
hostname = createReleaseConfig.hostname;
|
||||||
|
apiBaseUrl = createReleaseConfig.apiBaseUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract the 'user/repo' part from the URL
|
||||||
|
const escapedHostname = hostname.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||||
|
const regexString = `${escapedHostname}[/:]([\\w.-]+/[\\w.-]+)(\\.git)?`;
|
||||||
|
const regex = new RegExp(regexString);
|
||||||
|
const match = remoteUrl.match(regex);
|
||||||
|
|
||||||
|
if (match && match[1]) {
|
||||||
|
return {
|
||||||
|
hostname,
|
||||||
|
apiBaseUrl,
|
||||||
|
// Ensure any trailing .git is stripped
|
||||||
|
slug: match[1].replace(/\.git$/, '') as RemoteRepoSlug,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
`Could not extract "user/repo" data from the resolved remote URL: ${remoteUrl}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve a GitHub token from environment variables or gh CLI
|
||||||
|
*/
|
||||||
|
static async resolveTokenData(
|
||||||
|
hostname: string
|
||||||
|
): Promise<{ token: string; headerName: string } | null> {
|
||||||
|
// Try and resolve from the environment
|
||||||
|
const tokenFromEnv = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
|
||||||
|
if (tokenFromEnv) {
|
||||||
|
return { token: tokenFromEnv, headerName: 'Authorization' };
|
||||||
|
}
|
||||||
|
// Try and resolve from gh CLI installation
|
||||||
|
const ghCLIPath = joinPathFragments(
|
||||||
|
process.env.XDG_CONFIG_HOME || joinPathFragments(homedir(), '.config'),
|
||||||
|
'gh',
|
||||||
|
'hosts.yml'
|
||||||
|
);
|
||||||
|
if (existsSync(ghCLIPath)) {
|
||||||
|
const yamlContents = await fsp.readFile(ghCLIPath, 'utf8');
|
||||||
|
const { load } = require('@zkochan/js-yaml');
|
||||||
|
const ghCLIConfig = load(yamlContents);
|
||||||
|
if (ghCLIConfig[hostname]) {
|
||||||
|
// Web based session (the token is already embedded in the config)
|
||||||
|
if (ghCLIConfig[hostname].oauth_token) {
|
||||||
|
return ghCLIConfig[hostname].oauth_token;
|
||||||
|
}
|
||||||
|
// SSH based session (we need to dynamically resolve a token using the CLI)
|
||||||
|
if (
|
||||||
|
ghCLIConfig[hostname].user &&
|
||||||
|
ghCLIConfig[hostname].git_protocol === 'ssh'
|
||||||
|
) {
|
||||||
|
const token = execSync(`gh auth token`, {
|
||||||
|
encoding: 'utf8',
|
||||||
|
stdio: 'pipe',
|
||||||
|
windowsHide: false,
|
||||||
|
}).trim();
|
||||||
|
return { token, headerName: 'Authorization' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (hostname !== 'github.com') {
|
||||||
|
console.log(
|
||||||
|
`Warning: It was not possible to automatically resolve a GitHub token from your environment for hostname ${hostname}. If you set the GITHUB_TOKEN or GH_TOKEN environment variable, that will be used for GitHub API requests.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
createPostGitTask(
|
||||||
|
releaseVersion: ReleaseVersion,
|
||||||
|
changelogContents: string,
|
||||||
|
dryRun: boolean
|
||||||
|
): PostGitTask {
|
||||||
|
return async (latestCommit: string) => {
|
||||||
|
output.logSingleLine(`Creating GitHub Release`);
|
||||||
|
await this.createOrUpdateRelease(
|
||||||
|
releaseVersion,
|
||||||
|
changelogContents,
|
||||||
|
latestCommit,
|
||||||
|
{ dryRun }
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async applyUsernameToAuthors(
|
||||||
|
authors: Map<string, { email: Set<string>; username?: string }>
|
||||||
|
): Promise<void> {
|
||||||
|
await Promise.all(
|
||||||
|
[...authors.keys()].map(async (authorName) => {
|
||||||
|
const meta = authors.get(authorName);
|
||||||
|
for (const email of meta.email) {
|
||||||
|
if (email.endsWith('@users.noreply.github.com')) {
|
||||||
|
const match = email.match(
|
||||||
|
/^(\d+\+)?([^@]+)@users\.noreply\.github\.com$/
|
||||||
|
);
|
||||||
|
if (match && match[2]) {
|
||||||
|
meta.username = match[2];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const { data } = await axios
|
||||||
|
.get<any, { data?: { user?: { username: string } } }>(
|
||||||
|
`https://ungh.cc/users/find/${email}`
|
||||||
|
)
|
||||||
|
.catch(() => ({ data: { user: null } }));
|
||||||
|
if (data?.user) {
|
||||||
|
meta.username = data.user.username;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a release by tag
|
||||||
|
*/
|
||||||
|
protected async getReleaseByTag(tag: string): Promise<GithubRemoteRelease> {
|
||||||
|
const githubRepoData = this.getRequiredRemoteRepoData();
|
||||||
|
return await this.makeRequest(
|
||||||
|
`/repos/${githubRepoData.slug}/releases/tags/${tag}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new release
|
||||||
|
*/
|
||||||
|
protected async createRelease(
|
||||||
|
remoteRelease: GithubRemoteRelease
|
||||||
|
): Promise<any> {
|
||||||
|
const githubRepoData = this.getRequiredRemoteRepoData();
|
||||||
|
return await this.makeRequest(`/repos/${githubRepoData.slug}/releases`, {
|
||||||
|
method: 'POST',
|
||||||
|
data: remoteRelease,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async updateRelease(
|
||||||
|
id: string,
|
||||||
|
remoteRelease: GithubRemoteRelease
|
||||||
|
): Promise<any> {
|
||||||
|
const githubRepoData = this.getRequiredRemoteRepoData();
|
||||||
|
return await this.makeRequest(
|
||||||
|
`/repos/${githubRepoData.slug}/releases/${id}`,
|
||||||
|
{
|
||||||
|
method: 'PATCH',
|
||||||
|
data: remoteRelease,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected getManualRemoteReleaseURL(
|
||||||
|
remoteReleaseOptions: RemoteReleaseOptions
|
||||||
|
): string {
|
||||||
|
const githubRepoData = this.getRequiredRemoteRepoData();
|
||||||
|
// Parameters taken from https://github.com/isaacs/github/issues/1410#issuecomment-442240267
|
||||||
|
let url = `https://${githubRepoData.hostname}/${
|
||||||
|
githubRepoData.slug
|
||||||
|
}/releases/new?tag=${remoteReleaseOptions.version}&title=${
|
||||||
|
remoteReleaseOptions.version
|
||||||
|
}&body=${encodeURIComponent(remoteReleaseOptions.body)}&target=${
|
||||||
|
remoteReleaseOptions.commit
|
||||||
|
}`;
|
||||||
|
if (remoteReleaseOptions.prerelease) {
|
||||||
|
url += '&prerelease=true';
|
||||||
|
}
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected handleAuthError(): void {
|
||||||
|
output.error({
|
||||||
|
title: `Unable to resolve data via the GitHub API. You can use any of the following options to resolve this:`,
|
||||||
|
bodyLines: [
|
||||||
|
'- Set the `GITHUB_TOKEN` or `GH_TOKEN` environment variable to a valid GitHub token with `repo` scope',
|
||||||
|
'- Have an active session via the official gh CLI tool (https://cli.github.com) in your current terminal',
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected logReleaseAction(
|
||||||
|
existingRelease: GithubRemoteRelease | undefined,
|
||||||
|
gitTag: string,
|
||||||
|
dryRun: boolean
|
||||||
|
): void {
|
||||||
|
const githubRepoData = this.getRequiredRemoteRepoData();
|
||||||
|
const logTitle = `https://${githubRepoData.hostname}/${githubRepoData.slug}/releases/tag/${gitTag}`;
|
||||||
|
if (existingRelease) {
|
||||||
|
console.error(
|
||||||
|
`${chalk.white('UPDATE')} ${logTitle}${
|
||||||
|
dryRun ? chalk.keyword('orange')(' [dry-run]') : ''
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.error(
|
||||||
|
`${chalk.green('CREATE')} ${logTitle}${
|
||||||
|
dryRun ? chalk.keyword('orange')(' [dry-run]') : ''
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async handleError(
|
||||||
|
error: any,
|
||||||
|
result: RemoteReleaseResult
|
||||||
|
): Promise<void> {
|
||||||
|
if (error) {
|
||||||
|
process.exitCode = 1;
|
||||||
|
|
||||||
|
if (error.response?.data) {
|
||||||
|
// There's a nicely formatted error from GitHub we can display to the user
|
||||||
|
output.error({
|
||||||
|
title: `A GitHub API Error occurred when creating/updating the release`,
|
||||||
|
bodyLines: [
|
||||||
|
`GitHub Error: ${JSON.stringify(error.response.data)}`,
|
||||||
|
`---`,
|
||||||
|
`Request Data:`,
|
||||||
|
`Repo: ${this.getRemoteRepoData<GithubRepoData>()?.slug}`,
|
||||||
|
`Token Header Data: ${this.tokenHeader}`,
|
||||||
|
`Body: ${JSON.stringify(result.requestData)}`,
|
||||||
|
],
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log(error);
|
||||||
|
console.error(
|
||||||
|
`An unknown error occurred while trying to create a release on GitHub, please report this on https://github.com/nrwl/nx (NOTE: make sure to redact your GitHub token from the error message!)`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const shouldContinueInGitHub = await this.promptForContinueInGitHub();
|
||||||
|
if (!shouldContinueInGitHub) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const open = require('open');
|
||||||
|
await open(result.url)
|
||||||
|
.then(() => {
|
||||||
|
console.info(
|
||||||
|
`\nFollow up in the browser to manually create the release:\n\n` +
|
||||||
|
chalk.underline(chalk.cyan(result.url)) +
|
||||||
|
`\n`
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
console.info(
|
||||||
|
`Open this link to manually create a release: \n` +
|
||||||
|
chalk.underline(chalk.cyan(result.url)) +
|
||||||
|
'\n'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async promptForContinueInGitHub(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const reply = await prompt<{ open: 'Yes' | 'No' }>([
|
||||||
|
{
|
||||||
|
name: 'open',
|
||||||
|
message:
|
||||||
|
'Do you want to finish creating the release manually in your browser?',
|
||||||
|
type: 'autocomplete',
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
name: 'Yes',
|
||||||
|
hint: 'It will pre-populate the form for you',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'No',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
initial: 0,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
return reply.open === 'Yes';
|
||||||
|
} catch {
|
||||||
|
// Ensure the cursor is always restored before exiting
|
||||||
|
process.stdout.write('\u001b[?25h');
|
||||||
|
// Handle the case where the user exits the prompt with ctrl+c
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format references for the release (e.g., PRs, issues)
|
||||||
|
*/
|
||||||
|
formatReferences(references: Reference[]): string {
|
||||||
|
const githubRepoData = this.getRequiredRemoteRepoData();
|
||||||
|
const providerToRefSpec: Record<
|
||||||
|
string,
|
||||||
|
Record<Reference['type'], string>
|
||||||
|
> = {
|
||||||
|
github: { 'pull-request': 'pull', hash: 'commit', issue: 'issues' },
|
||||||
|
};
|
||||||
|
|
||||||
|
const refSpec = providerToRefSpec.github;
|
||||||
|
|
||||||
|
const formatSingleReference = (ref: Reference) => {
|
||||||
|
return `[${ref.value}](https://${githubRepoData.hostname}/${
|
||||||
|
githubRepoData.slug
|
||||||
|
}/${refSpec[ref.type]}/${ref.value.replace(/^#/, '')})`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const pr = references.filter((ref) => ref.type === 'pull-request');
|
||||||
|
const issue = references.filter((ref) => ref.type === 'issue');
|
||||||
|
|
||||||
|
if (pr.length > 0 || issue.length > 0) {
|
||||||
|
return (
|
||||||
|
' (' +
|
||||||
|
[...pr, ...issue].map((ref) => formatSingleReference(ref)).join(', ') +
|
||||||
|
')'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (references.length > 0) {
|
||||||
|
return ' (' + formatSingleReference(references[0]) + ')';
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async syncRelease(
|
||||||
|
remoteReleaseOptions: RemoteReleaseOptions,
|
||||||
|
existingRelease?: GithubRemoteRelease
|
||||||
|
): Promise<RemoteReleaseResult> {
|
||||||
|
const githubReleaseData: GithubRemoteRelease = {
|
||||||
|
tag_name: remoteReleaseOptions.version,
|
||||||
|
name: remoteReleaseOptions.version,
|
||||||
|
body: remoteReleaseOptions.body,
|
||||||
|
prerelease: remoteReleaseOptions.prerelease,
|
||||||
|
// legacy specifies that the latest release should be determined based on the release creation date and higher semantic version.
|
||||||
|
make_latest: 'legacy',
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const newGhRelease = await (existingRelease
|
||||||
|
? this.updateRelease(existingRelease.id, githubReleaseData)
|
||||||
|
: this.createRelease({
|
||||||
|
...githubReleaseData,
|
||||||
|
target_commitish: remoteReleaseOptions.commit,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: existingRelease ? 'updated' : 'created',
|
||||||
|
id: newGhRelease.id,
|
||||||
|
url: newGhRelease.html_url,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
status: 'manual',
|
||||||
|
error,
|
||||||
|
url: this.getManualRemoteReleaseURL(remoteReleaseOptions),
|
||||||
|
requestData: githubReleaseData,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getRequiredRemoteRepoData(): GithubRepoData {
|
||||||
|
const githubRepoData = this.getRemoteRepoData<GithubRepoData>();
|
||||||
|
if (!githubRepoData) {
|
||||||
|
throw new Error(
|
||||||
|
`No remote repo data could be resolved for the current workspace`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return githubRepoData;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,404 @@
|
|||||||
|
import * as chalk from 'chalk';
|
||||||
|
import { prompt } from 'enquirer';
|
||||||
|
import { execSync } from 'node:child_process';
|
||||||
|
import { output } from '../../../../utils/output';
|
||||||
|
import type { PostGitTask } from '../../changelog';
|
||||||
|
import type { ResolvedCreateRemoteReleaseProvider } from '../../config/config';
|
||||||
|
import type { Reference } from '../git';
|
||||||
|
import { ReleaseVersion } from '../shared';
|
||||||
|
import {
|
||||||
|
RemoteReleaseClient,
|
||||||
|
RemoteReleaseOptions,
|
||||||
|
RemoteReleaseResult,
|
||||||
|
RemoteRepoData,
|
||||||
|
RemoteRepoSlug,
|
||||||
|
} from './remote-release-client';
|
||||||
|
|
||||||
|
export interface GitLabRepoData extends RemoteRepoData {
|
||||||
|
projectId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://docs.gitlab.com/api/releases/#create-a-release
|
||||||
|
export interface GitLabRelease {
|
||||||
|
id?: string;
|
||||||
|
name?: string;
|
||||||
|
tag_name: string;
|
||||||
|
ref: string;
|
||||||
|
assets?: {
|
||||||
|
links?: {
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
direct_asset_path?: string;
|
||||||
|
link_type?: string;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
released_at?: string;
|
||||||
|
description?: string;
|
||||||
|
milestones?: string[];
|
||||||
|
prerelease?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaultCreateReleaseProvider: ResolvedCreateRemoteReleaseProvider =
|
||||||
|
{
|
||||||
|
provider: 'gitlab',
|
||||||
|
hostname: 'gitlab.com',
|
||||||
|
apiBaseUrl: 'https://gitlab.com/api/v4',
|
||||||
|
};
|
||||||
|
|
||||||
|
export class GitLabRemoteReleaseClient extends RemoteReleaseClient<GitLabRelease> {
|
||||||
|
remoteReleaseProviderName = 'GitLab';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get GitLab repository data from git remote
|
||||||
|
*/
|
||||||
|
static resolveRepoData(
|
||||||
|
createReleaseConfig: false | ResolvedCreateRemoteReleaseProvider,
|
||||||
|
remoteName = 'origin'
|
||||||
|
): GitLabRepoData | null {
|
||||||
|
try {
|
||||||
|
const remoteUrl = execSync(`git remote get-url ${remoteName}`, {
|
||||||
|
encoding: 'utf8',
|
||||||
|
stdio: 'pipe',
|
||||||
|
}).trim();
|
||||||
|
|
||||||
|
// Use the default provider if custom one is not specified or releases are disabled
|
||||||
|
let hostname = defaultCreateReleaseProvider.hostname;
|
||||||
|
let apiBaseUrl = defaultCreateReleaseProvider.apiBaseUrl;
|
||||||
|
|
||||||
|
if (
|
||||||
|
createReleaseConfig !== false &&
|
||||||
|
typeof createReleaseConfig !== 'string'
|
||||||
|
) {
|
||||||
|
hostname = createReleaseConfig.hostname || hostname;
|
||||||
|
apiBaseUrl = createReleaseConfig.apiBaseUrl || apiBaseUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract the project path from the URL
|
||||||
|
const escapedHostname = hostname.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||||
|
const regexString = `${escapedHostname}[/:]([\\w.-]+/[\\w.-]+(?:/[\\w.-]+)*)(\\.git)?`;
|
||||||
|
const regex = new RegExp(regexString);
|
||||||
|
const match = remoteUrl.match(regex);
|
||||||
|
|
||||||
|
if (match && match[1]) {
|
||||||
|
// Remove trailing .git if present
|
||||||
|
const slug = match[1].replace(/\.git$/, '') as RemoteRepoSlug;
|
||||||
|
|
||||||
|
// Encode the project path for use in API URLs
|
||||||
|
const projectId = encodeURIComponent(slug);
|
||||||
|
|
||||||
|
return {
|
||||||
|
hostname,
|
||||||
|
apiBaseUrl,
|
||||||
|
slug,
|
||||||
|
projectId,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
`Could not extract project path data from the resolved remote URL: ${remoteUrl}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (process.env.NX_VERBOSE_LOGGING === 'true') {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve a GitLab token from various environment variables
|
||||||
|
*/
|
||||||
|
static async resolveTokenData(
|
||||||
|
hostname: string
|
||||||
|
): Promise<{ token: string; headerName: string } | null> {
|
||||||
|
// Try and resolve from the environment
|
||||||
|
const tokenFromEnv = process.env.GITLAB_TOKEN || process.env.GL_TOKEN;
|
||||||
|
if (tokenFromEnv) {
|
||||||
|
return { token: tokenFromEnv, headerName: 'PRIVATE-TOKEN' };
|
||||||
|
}
|
||||||
|
// Try and resolve from a CI environment
|
||||||
|
if (process.env.CI_JOB_TOKEN) {
|
||||||
|
return { token: process.env.CI_JOB_TOKEN, headerName: 'JOB-TOKEN' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hostname !== 'gitlab.com') {
|
||||||
|
console.log(
|
||||||
|
`Warning: It was not possible to automatically resolve a GitLab token from your environment for hostname ${hostname}. If you set the GITLAB_TOKEN or GL_TOKEN environment variable (or you are in GitLab CI where CI_JOB_TOKEN is set automatically), that will be used for GitLab API requests.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
createPostGitTask(
|
||||||
|
releaseVersion: ReleaseVersion,
|
||||||
|
changelogContents: string,
|
||||||
|
dryRun: boolean
|
||||||
|
): PostGitTask {
|
||||||
|
return async (latestCommit: string) => {
|
||||||
|
output.logSingleLine(`Creating GitLab Release`);
|
||||||
|
await this.createOrUpdateRelease(
|
||||||
|
releaseVersion,
|
||||||
|
changelogContents,
|
||||||
|
latestCommit,
|
||||||
|
{ dryRun }
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not implemented for GitLab yet, the changelog renderer should not call this method
|
||||||
|
async applyUsernameToAuthors(): Promise<void> {
|
||||||
|
throw new Error('applyUsernameToAuthors is not implemented for GitLab yet');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async getReleaseByTag(tag: string): Promise<GitLabRelease> {
|
||||||
|
const gitlabRepoData = this.getRequiredRemoteRepoData();
|
||||||
|
return await this.makeRequest(
|
||||||
|
`/projects/${gitlabRepoData.projectId}/releases/${encodeURIComponent(
|
||||||
|
tag
|
||||||
|
)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async createRelease(remoteRelease: GitLabRelease): Promise<any> {
|
||||||
|
const gitlabRepoData = this.getRequiredRemoteRepoData();
|
||||||
|
return await this.makeRequest(
|
||||||
|
`/projects/${gitlabRepoData.projectId}/releases`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
data: remoteRelease,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async updateRelease(
|
||||||
|
_id: string,
|
||||||
|
remoteRelease: GitLabRelease
|
||||||
|
): Promise<any> {
|
||||||
|
const gitlabRepoData = this.getRequiredRemoteRepoData();
|
||||||
|
return await this.makeRequest(
|
||||||
|
`/projects/${gitlabRepoData.projectId}/releases/${encodeURIComponent(
|
||||||
|
remoteRelease.tag_name
|
||||||
|
)}`,
|
||||||
|
{
|
||||||
|
method: 'PUT',
|
||||||
|
data: remoteRelease,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a URL for manual release creation on GitLab. Sadly, unlike GitHub, GitLab does not
|
||||||
|
* seem to respect query string parameters for setting the UI form fields, so the user has to
|
||||||
|
* start from scratch.
|
||||||
|
*/
|
||||||
|
protected getManualRemoteReleaseURL(
|
||||||
|
_remoteReleaseOptions: RemoteReleaseOptions
|
||||||
|
): string {
|
||||||
|
const gitlabRepoData = this.getRequiredRemoteRepoData();
|
||||||
|
return `https://${gitlabRepoData.hostname}/${gitlabRepoData.slug}/-/releases/new`;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected handleAuthError(): void {
|
||||||
|
output.error({
|
||||||
|
title: `Unable to resolve data via the GitLab API.`,
|
||||||
|
bodyLines: [
|
||||||
|
'- Set the `GITLAB_TOKEN` or `GL_TOKEN` environment variable to a valid GitLab token with `api` scope',
|
||||||
|
'- If running in GitLab CI, the automatically provisioned CI_JOB_TOKEN can also be used',
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected logReleaseAction(
|
||||||
|
existingRelease: GitLabRelease | undefined,
|
||||||
|
gitTag: string,
|
||||||
|
dryRun: boolean
|
||||||
|
): void {
|
||||||
|
const gitlabRepoData = this.getRequiredRemoteRepoData();
|
||||||
|
const logTitle = `https://${gitlabRepoData.hostname}/${
|
||||||
|
gitlabRepoData.slug
|
||||||
|
}/-/releases/${encodeURIComponent(gitTag)}`;
|
||||||
|
if (existingRelease) {
|
||||||
|
console.error(
|
||||||
|
`${chalk.white('UPDATE')} ${logTitle}${
|
||||||
|
dryRun ? chalk.keyword('orange')(' [dry-run]') : ''
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.error(
|
||||||
|
`${chalk.green('CREATE')} ${logTitle}${
|
||||||
|
dryRun ? chalk.keyword('orange')(' [dry-run]') : ''
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async handleError(
|
||||||
|
error: any,
|
||||||
|
result: RemoteReleaseResult
|
||||||
|
): Promise<void> {
|
||||||
|
if (error) {
|
||||||
|
process.exitCode = 1;
|
||||||
|
|
||||||
|
if (error.response?.data) {
|
||||||
|
output.error({
|
||||||
|
title: `A GitLab API Error occurred when creating/updating the release`,
|
||||||
|
bodyLines: [
|
||||||
|
`GitLab Error: ${JSON.stringify(error.response.data)}`,
|
||||||
|
`---`,
|
||||||
|
`Request Data:`,
|
||||||
|
`Repo: ${this.getRemoteRepoData<GitLabRepoData>()?.slug}`,
|
||||||
|
`Token Header Data: ${this.tokenHeader}`,
|
||||||
|
`Body: ${JSON.stringify(result.requestData)}`,
|
||||||
|
],
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log(error);
|
||||||
|
console.error(
|
||||||
|
`An unknown error occurred while trying to create a release on GitLab, please report this on https://github.com/nrwl/nx (NOTE: make sure to redact your GitLab token from the error message!)`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const shouldContinueInGitLab = await this.promptForContinueInGitLab();
|
||||||
|
if (!shouldContinueInGitLab) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const open = require('open');
|
||||||
|
await open(result.url)
|
||||||
|
.then(() => {
|
||||||
|
console.info(
|
||||||
|
`\nFollow up in the browser to manually create the release:\n\n` +
|
||||||
|
chalk.underline(chalk.cyan(result.url)) +
|
||||||
|
`\n`
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
console.info(
|
||||||
|
`Open this link to manually create a release: \n` +
|
||||||
|
chalk.underline(chalk.cyan(result.url)) +
|
||||||
|
'\n'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async promptForContinueInGitLab(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const reply = await prompt<{ open: 'Yes' | 'No' }>([
|
||||||
|
{
|
||||||
|
name: 'open',
|
||||||
|
message:
|
||||||
|
'Do you want to create the release manually in your browser?',
|
||||||
|
type: 'autocomplete',
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
name: 'Yes',
|
||||||
|
hint: 'It will open the GitLab release page for you',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'No',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
initial: 0,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
return reply.open === 'Yes';
|
||||||
|
} catch {
|
||||||
|
// Ensure the cursor is always restored before exiting
|
||||||
|
process.stdout.write('\u001b[?25h');
|
||||||
|
// Handle the case where the user exits the prompt with ctrl+c
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format references for the release (e.g., MRs, issues)
|
||||||
|
*/
|
||||||
|
formatReferences(references: Reference[]): string {
|
||||||
|
const gitlabRepoData = this.getRequiredRemoteRepoData();
|
||||||
|
const providerToRefSpec: Record<
|
||||||
|
string,
|
||||||
|
Record<Reference['type'], string>
|
||||||
|
> = {
|
||||||
|
gitlab: {
|
||||||
|
'pull-request': 'merge_requests',
|
||||||
|
hash: 'commit',
|
||||||
|
issue: 'issues',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const refSpec = providerToRefSpec.gitlab;
|
||||||
|
|
||||||
|
const formatSingleReference = (ref: Reference) => {
|
||||||
|
return `https://${gitlabRepoData.hostname}/${gitlabRepoData.slug}/-/${
|
||||||
|
refSpec[ref.type]
|
||||||
|
}/${ref.value.replace(/^[#!]/, '')}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const mr = references.filter((ref) => ref.type === 'pull-request');
|
||||||
|
const issue = references.filter((ref) => ref.type === 'issue');
|
||||||
|
|
||||||
|
if (mr.length > 0 || issue.length > 0) {
|
||||||
|
return (
|
||||||
|
' (' +
|
||||||
|
[...mr, ...issue].map((ref) => formatSingleReference(ref)).join(', ') +
|
||||||
|
')'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (references.length > 0) {
|
||||||
|
return ' (' + formatSingleReference(references[0]) + ')';
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async syncRelease(
|
||||||
|
remoteReleaseOptions: RemoteReleaseOptions,
|
||||||
|
existingRelease?: GitLabRelease
|
||||||
|
): Promise<RemoteReleaseResult> {
|
||||||
|
const gitlabReleaseData: GitLabRelease = {
|
||||||
|
tag_name: remoteReleaseOptions.version,
|
||||||
|
name: remoteReleaseOptions.version,
|
||||||
|
description: remoteReleaseOptions.body,
|
||||||
|
prerelease: remoteReleaseOptions.prerelease,
|
||||||
|
ref: remoteReleaseOptions.commit,
|
||||||
|
released_at: new Date().toISOString(),
|
||||||
|
assets: { links: [] },
|
||||||
|
milestones: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const newGlRelease = await (existingRelease
|
||||||
|
? this.updateRelease(existingRelease.id, gitlabReleaseData)
|
||||||
|
: this.createRelease(gitlabReleaseData));
|
||||||
|
|
||||||
|
const gitlabRepoData = this.getRequiredRemoteRepoData();
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: existingRelease ? 'updated' : 'created',
|
||||||
|
id: newGlRelease.tag_name,
|
||||||
|
url: `https://${gitlabRepoData.hostname}/${
|
||||||
|
gitlabRepoData.slug
|
||||||
|
}/-/tags/${encodeURIComponent(remoteReleaseOptions.version)}`,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
status: 'manual',
|
||||||
|
error,
|
||||||
|
url: this.getManualRemoteReleaseURL(remoteReleaseOptions),
|
||||||
|
requestData: gitlabReleaseData,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getRequiredRemoteRepoData(): GitLabRepoData {
|
||||||
|
const gitlabRepoData = this.getRemoteRepoData<GitLabRepoData>();
|
||||||
|
if (!gitlabRepoData) {
|
||||||
|
throw new Error(
|
||||||
|
`No remote repo data could be resolved for the current workspace`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return gitlabRepoData;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,292 @@
|
|||||||
|
import type { AxiosRequestConfig } from 'axios';
|
||||||
|
import axios from 'axios';
|
||||||
|
import type { PostGitTask } from '../../changelog';
|
||||||
|
import { ResolvedCreateRemoteReleaseProvider } from '../../config/config';
|
||||||
|
import type { Reference } from '../git';
|
||||||
|
import { printDiff } from '../print-changes';
|
||||||
|
import { noDiffInChangelogMessage, type ReleaseVersion } from '../shared';
|
||||||
|
import type { GithubRemoteReleaseClient } from './github';
|
||||||
|
import type { GitLabRemoteReleaseClient } from './gitlab';
|
||||||
|
|
||||||
|
export type RemoteRepoSlug = `${string}/${string}`;
|
||||||
|
|
||||||
|
// Base repository data interface
|
||||||
|
export interface RemoteRepoData {
|
||||||
|
hostname: string;
|
||||||
|
slug: RemoteRepoSlug;
|
||||||
|
apiBaseUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Release options for creating or updating releases
|
||||||
|
export interface RemoteReleaseOptions {
|
||||||
|
version: string;
|
||||||
|
body: string;
|
||||||
|
prerelease: boolean;
|
||||||
|
commit: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Release creation result
|
||||||
|
export interface RemoteReleaseResult {
|
||||||
|
status: 'created' | 'updated' | 'manual';
|
||||||
|
id?: string;
|
||||||
|
url?: string;
|
||||||
|
error?: any;
|
||||||
|
requestData?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract base class for remote release clients
|
||||||
|
*/
|
||||||
|
export abstract class RemoteReleaseClient<
|
||||||
|
RemoteRelease extends Record<string, any>
|
||||||
|
> {
|
||||||
|
/**
|
||||||
|
* Used in user-facing messaging
|
||||||
|
*/
|
||||||
|
abstract remoteReleaseProviderName: string;
|
||||||
|
protected tokenHeader: Record<string, string>;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
// A workspace isn't guaranteed to have a remote
|
||||||
|
private remoteRepoData: RemoteRepoData | null,
|
||||||
|
protected createReleaseConfig: false | ResolvedCreateRemoteReleaseProvider,
|
||||||
|
protected tokenData: { token: string; headerName: string } | null
|
||||||
|
) {
|
||||||
|
this.tokenHeader = {};
|
||||||
|
if (tokenData) {
|
||||||
|
if (tokenData.headerName === 'Authorization') {
|
||||||
|
this.tokenHeader[tokenData.headerName] = `Bearer ${tokenData.token}`;
|
||||||
|
} else {
|
||||||
|
this.tokenHeader[tokenData.headerName] = tokenData.token;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getRemoteRepoData<T extends RemoteRepoData>(): T | null {
|
||||||
|
return this.remoteRepoData as T | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a post git task that will be executed by nx release changelog after performing any relevant
|
||||||
|
* git operations, if the user has opted into remote release creation.
|
||||||
|
*/
|
||||||
|
abstract createPostGitTask(
|
||||||
|
releaseVersion: ReleaseVersion,
|
||||||
|
changelogContents: string,
|
||||||
|
dryRun: boolean
|
||||||
|
): PostGitTask;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply authors' corresponding usernames, if applicable, on the remote release provider. It is designed to be
|
||||||
|
* invoked by a changelog renderer implementation.
|
||||||
|
*/
|
||||||
|
abstract applyUsernameToAuthors(
|
||||||
|
authors: Map<string, { email: Set<string>; username?: string }>
|
||||||
|
): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make an (optionally authenticated) API request to the remote release provider
|
||||||
|
*/
|
||||||
|
protected async makeRequest(
|
||||||
|
url: string,
|
||||||
|
opts: AxiosRequestConfig = {}
|
||||||
|
): Promise<any> {
|
||||||
|
const remoteRepoData = this.getRemoteRepoData<RemoteRepoData>();
|
||||||
|
if (!remoteRepoData) {
|
||||||
|
throw new Error(
|
||||||
|
`No remote repo data could be resolved for the current workspace`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const config: AxiosRequestConfig<any> = {
|
||||||
|
...opts,
|
||||||
|
baseURL: remoteRepoData.apiBaseUrl,
|
||||||
|
headers: {
|
||||||
|
...(opts.headers as any),
|
||||||
|
...this.tokenHeader,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return (await axios<any, any>(url, config)).data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async createOrUpdateRelease(
|
||||||
|
releaseVersion: ReleaseVersion,
|
||||||
|
changelogContents: string,
|
||||||
|
latestCommit: string,
|
||||||
|
{ dryRun }: { dryRun: boolean }
|
||||||
|
): Promise<void> {
|
||||||
|
let existingRelease: RemoteRelease | undefined;
|
||||||
|
|
||||||
|
try {
|
||||||
|
existingRelease = await this.getReleaseByTag(releaseVersion.gitTag);
|
||||||
|
} catch (err) {
|
||||||
|
if (err.response?.status === 401) {
|
||||||
|
this.handleAuthError();
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
if (err.response?.status === 404) {
|
||||||
|
// No existing release found, this is fine
|
||||||
|
} else {
|
||||||
|
// Rethrow unknown errors for now
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logReleaseAction(existingRelease, releaseVersion.gitTag, dryRun);
|
||||||
|
|
||||||
|
this.printRemoteReleaseContents(
|
||||||
|
existingRelease
|
||||||
|
? 'body' in existingRelease
|
||||||
|
? existingRelease.body
|
||||||
|
: 'description' in existingRelease
|
||||||
|
? existingRelease.description
|
||||||
|
: ''
|
||||||
|
: '',
|
||||||
|
changelogContents
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!dryRun) {
|
||||||
|
const remoteReleaseOptions: RemoteReleaseOptions = {
|
||||||
|
version: releaseVersion.gitTag,
|
||||||
|
prerelease: releaseVersion.isPrerelease,
|
||||||
|
body: changelogContents,
|
||||||
|
commit: latestCommit,
|
||||||
|
};
|
||||||
|
const result = await this.syncRelease(
|
||||||
|
remoteReleaseOptions,
|
||||||
|
existingRelease
|
||||||
|
);
|
||||||
|
if (result.status === 'manual') {
|
||||||
|
await this.handleError(result.error, result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format references for the release (e.g., PRs, issues)
|
||||||
|
*/
|
||||||
|
formatReferences(_references: Reference[]): string {
|
||||||
|
// Base implementation - to be overridden by specific providers
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle non-auth related errors when creating releases
|
||||||
|
*/
|
||||||
|
protected abstract handleError(
|
||||||
|
error: any,
|
||||||
|
result: RemoteReleaseResult
|
||||||
|
): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display authentication error message
|
||||||
|
*/
|
||||||
|
protected abstract handleAuthError(): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log the release action (create or update)
|
||||||
|
*/
|
||||||
|
protected abstract logReleaseAction(
|
||||||
|
existingRelease: RemoteRelease | undefined,
|
||||||
|
gitTag: string,
|
||||||
|
dryRun: boolean
|
||||||
|
): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Print changelog changes
|
||||||
|
*/
|
||||||
|
protected printRemoteReleaseContents(
|
||||||
|
existingBody: string,
|
||||||
|
newBody: string
|
||||||
|
): void {
|
||||||
|
console.log('');
|
||||||
|
printDiff(existingBody, newBody, 3, noDiffInChangelogMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a release by tag
|
||||||
|
*/
|
||||||
|
protected abstract getReleaseByTag(tag: string): Promise<RemoteRelease>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a manual release URL used to create/edit a release in the remote release provider's UI
|
||||||
|
*/
|
||||||
|
protected abstract getManualRemoteReleaseURL(
|
||||||
|
remoteReleaseOptions: RemoteReleaseOptions
|
||||||
|
): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new release
|
||||||
|
*/
|
||||||
|
protected abstract createRelease(body: RemoteRelease): Promise<any>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update an existing release
|
||||||
|
*/
|
||||||
|
protected abstract updateRelease(
|
||||||
|
id: string,
|
||||||
|
body: RemoteRelease
|
||||||
|
): Promise<any>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Synchronize a release with the remote release provider
|
||||||
|
*/
|
||||||
|
protected abstract syncRelease(
|
||||||
|
remoteReleaseOptions: RemoteReleaseOptions,
|
||||||
|
existingRelease?: RemoteRelease
|
||||||
|
): Promise<RemoteReleaseResult>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factory function to create a remote release client based on the given configuration
|
||||||
|
*/
|
||||||
|
export async function createRemoteReleaseClient(
|
||||||
|
createReleaseConfig: false | ResolvedCreateRemoteReleaseProvider,
|
||||||
|
remoteName = 'origin'
|
||||||
|
): Promise<GithubRemoteReleaseClient | GitLabRemoteReleaseClient | null> {
|
||||||
|
switch (true) {
|
||||||
|
// GitHub and GitHub Enterprise Server
|
||||||
|
case typeof createReleaseConfig === 'object' &&
|
||||||
|
(createReleaseConfig.provider === 'github-enterprise-server' ||
|
||||||
|
createReleaseConfig.provider === 'github'):
|
||||||
|
// If remote releases are disabled, assume GitHub repo data resolution (but don't attempt to resolve a token) to match existing behavior
|
||||||
|
case createReleaseConfig === false: {
|
||||||
|
const { GithubRemoteReleaseClient } = await import('./github');
|
||||||
|
const repoData = GithubRemoteReleaseClient.resolveRepoData(
|
||||||
|
createReleaseConfig,
|
||||||
|
remoteName
|
||||||
|
);
|
||||||
|
const token =
|
||||||
|
createReleaseConfig && repoData
|
||||||
|
? await GithubRemoteReleaseClient.resolveTokenData(repoData.hostname)
|
||||||
|
: null;
|
||||||
|
return new GithubRemoteReleaseClient(
|
||||||
|
repoData,
|
||||||
|
createReleaseConfig,
|
||||||
|
token
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// GitLab
|
||||||
|
case typeof createReleaseConfig === 'object' &&
|
||||||
|
createReleaseConfig.provider === 'gitlab': {
|
||||||
|
const { GitLabRemoteReleaseClient } = await import('./gitlab');
|
||||||
|
const repoData = GitLabRemoteReleaseClient.resolveRepoData(
|
||||||
|
createReleaseConfig,
|
||||||
|
remoteName
|
||||||
|
);
|
||||||
|
const tokenData = repoData
|
||||||
|
? await GitLabRemoteReleaseClient.resolveTokenData(repoData.hostname)
|
||||||
|
: null;
|
||||||
|
return new GitLabRemoteReleaseClient(
|
||||||
|
repoData,
|
||||||
|
createReleaseConfig,
|
||||||
|
tokenData
|
||||||
|
);
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw new Error(
|
||||||
|
`Unsupported remote release configuration: ${JSON.stringify(
|
||||||
|
createReleaseConfig
|
||||||
|
)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -191,6 +191,7 @@ export interface NxReleaseChangelogConfiguration {
|
|||||||
createRelease?:
|
createRelease?:
|
||||||
| false
|
| false
|
||||||
| 'github'
|
| 'github'
|
||||||
|
| 'gitlab'
|
||||||
| {
|
| {
|
||||||
provider: 'github-enterprise-server';
|
provider: 'github-enterprise-server';
|
||||||
hostname: string;
|
hostname: string;
|
||||||
@ -198,6 +199,14 @@ export interface NxReleaseChangelogConfiguration {
|
|||||||
* If not set, this will default to `https://${hostname}/api/v3`
|
* If not set, this will default to `https://${hostname}/api/v3`
|
||||||
*/
|
*/
|
||||||
apiBaseUrl?: string;
|
apiBaseUrl?: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
provider: 'gitlab';
|
||||||
|
hostname: string;
|
||||||
|
/**
|
||||||
|
* If not set, this will default to `https://${hostname}/api/v4`
|
||||||
|
*/
|
||||||
|
apiBaseUrl?: string;
|
||||||
};
|
};
|
||||||
/**
|
/**
|
||||||
* This can either be set to a string value that will be written to the changelog file(s)
|
* This can either be set to a string value that will be written to the changelog file(s)
|
||||||
|
|||||||
@ -0,0 +1,54 @@
|
|||||||
|
#### Nx Release Changelog Config Changes
|
||||||
|
|
||||||
|
In Nx v21, the `mapAuthorsToGitHubUsernames` changelog "renderOption" for the default changelog renderer was renamed to `applyUsernameToAuthors` to reflect the fact that it is no longer specific to GitHub. Most people were not setting this option directly, but if you were, it will be automatically migrated by this migration.
|
||||||
|
|
||||||
|
The migration will also update release groups changelog configuration, if applicable.
|
||||||
|
|
||||||
|
#### Sample Code Changes
|
||||||
|
|
||||||
|
{% tabs %}
|
||||||
|
{% tab label="Before" %}
|
||||||
|
|
||||||
|
```json {% fileName="nx.json" %}
|
||||||
|
{
|
||||||
|
"release": {
|
||||||
|
"changelog": {
|
||||||
|
"workspaceChangelog": {
|
||||||
|
"renderOptions": {
|
||||||
|
"mapAuthorsToGitHubUsernames": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"projectChangelogs": {
|
||||||
|
"renderOptions": {
|
||||||
|
"mapAuthorsToGitHubUsernames": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
{% /tab %}
|
||||||
|
{% tab label="After" %}
|
||||||
|
|
||||||
|
```json {% fileName="nx.json" %}
|
||||||
|
{
|
||||||
|
"release": {
|
||||||
|
"changelog": {
|
||||||
|
"workspaceChangelog": {
|
||||||
|
"renderOptions": {
|
||||||
|
"applyUsernameToAuthors": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"projectChangelogs": {
|
||||||
|
"renderOptions": {
|
||||||
|
"applyUsernameToAuthors": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
{% /tab %}
|
||||||
|
{% /tabs %}
|
||||||
@ -0,0 +1,93 @@
|
|||||||
|
import { createTreeWithEmptyWorkspace } from '../../generators/testing-utils/create-tree-with-empty-workspace';
|
||||||
|
import { Tree } from '../../generators/tree';
|
||||||
|
import { readNxJson, updateNxJson } from '../../generators/utils/nx-json';
|
||||||
|
|
||||||
|
import update from './release-changelog-config-changes';
|
||||||
|
|
||||||
|
describe('release changelog config changes', () => {
|
||||||
|
let tree: Tree;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
tree = createTreeWithEmptyWorkspace();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should do nothing when nxJson.release is not set', async () => {
|
||||||
|
updateNxJson(tree, {});
|
||||||
|
await update(tree);
|
||||||
|
expect(readNxJson(tree)).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should do nothing when mapAuthorsToGitHubUsernames is not set', async () => {
|
||||||
|
const nxJsonBefore = {
|
||||||
|
release: {
|
||||||
|
changelog: {
|
||||||
|
workspaceChangelog: {
|
||||||
|
renderOptions: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
updateNxJson(tree, nxJsonBefore);
|
||||||
|
|
||||||
|
await update(tree);
|
||||||
|
|
||||||
|
expect(readNxJson(tree)).toEqual(nxJsonBefore);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update mapAuthorsToGitHubUsernames to applyUsernameToAuthors if set at any level', async () => {
|
||||||
|
updateNxJson(tree, {
|
||||||
|
release: {
|
||||||
|
changelog: {
|
||||||
|
workspaceChangelog: {
|
||||||
|
renderOptions: { mapAuthorsToGitHubUsernames: true },
|
||||||
|
},
|
||||||
|
projectChangelogs: {
|
||||||
|
renderOptions: { mapAuthorsToGitHubUsernames: false },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
groups: {
|
||||||
|
group1: {
|
||||||
|
projects: ['project1', 'project2'],
|
||||||
|
changelog: {
|
||||||
|
renderOptions: { mapAuthorsToGitHubUsernames: true },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await update(tree);
|
||||||
|
|
||||||
|
expect(readNxJson(tree)).toMatchInlineSnapshot(`
|
||||||
|
{
|
||||||
|
"release": {
|
||||||
|
"changelog": {
|
||||||
|
"projectChangelogs": {
|
||||||
|
"renderOptions": {
|
||||||
|
"applyUsernameToAuthors": false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"workspaceChangelog": {
|
||||||
|
"renderOptions": {
|
||||||
|
"applyUsernameToAuthors": true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"groups": {
|
||||||
|
"group1": {
|
||||||
|
"changelog": {
|
||||||
|
"renderOptions": {
|
||||||
|
"applyUsernameToAuthors": true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"projects": [
|
||||||
|
"project1",
|
||||||
|
"project2",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,53 @@
|
|||||||
|
import {
|
||||||
|
NxJsonConfiguration,
|
||||||
|
NxReleaseChangelogConfiguration,
|
||||||
|
NxReleaseVersionConfiguration,
|
||||||
|
} from '../../config/nx-json';
|
||||||
|
import { formatChangedFilesWithPrettierIfAvailable } from '../../generators/internal-utils/format-changed-files-with-prettier-if-available';
|
||||||
|
import { Tree } from '../../generators/tree';
|
||||||
|
import { readNxJson, updateNxJson } from '../../generators/utils/nx-json';
|
||||||
|
|
||||||
|
export default async function update(tree: Tree) {
|
||||||
|
const nxJson = readNxJson(tree) as NxJsonConfiguration;
|
||||||
|
if (!nxJson || !nxJson.release) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateProperty(changelogConfig: NxReleaseChangelogConfiguration) {
|
||||||
|
if (
|
||||||
|
changelogConfig.renderOptions &&
|
||||||
|
'mapAuthorsToGitHubUsernames' in changelogConfig.renderOptions
|
||||||
|
) {
|
||||||
|
changelogConfig.renderOptions.applyUsernameToAuthors =
|
||||||
|
changelogConfig.renderOptions.mapAuthorsToGitHubUsernames;
|
||||||
|
delete changelogConfig.renderOptions.mapAuthorsToGitHubUsernames;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nxJson.release.changelog) {
|
||||||
|
if (
|
||||||
|
nxJson.release.changelog.workspaceChangelog &&
|
||||||
|
typeof nxJson.release.changelog.workspaceChangelog !== 'boolean'
|
||||||
|
) {
|
||||||
|
updateProperty(nxJson.release.changelog.workspaceChangelog);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
nxJson.release.changelog.projectChangelogs &&
|
||||||
|
typeof nxJson.release.changelog.projectChangelogs !== 'boolean'
|
||||||
|
) {
|
||||||
|
updateProperty(nxJson.release.changelog.projectChangelogs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nxJson.release.groups) {
|
||||||
|
for (const group of Object.values(nxJson.release.groups)) {
|
||||||
|
if (group.changelog && typeof group.changelog !== 'boolean') {
|
||||||
|
updateProperty(group.changelog);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateNxJson(tree, nxJson);
|
||||||
|
|
||||||
|
await formatChangedFilesWithPrettierIfAvailable(tree);
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user