chore: enforce changelog entries on PR reviews (#25459)

Co-authored-by: Blue F <blue@cypress.io>
This commit is contained in:
Emily Rohrbough
2023-01-24 15:30:33 -06:00
committed by GitHub
parent 0ae4b678f9
commit a869d5dd28
34 changed files with 1609 additions and 87 deletions

View File

@@ -41,6 +41,7 @@ macWorkflowFilters: &darwin-workflow-filters
- matches:
pattern: /^release\/\d+\.\d+\.\d+$/
value: << pipeline.git.branch >>
linuxArm64WorkflowFilters: &linux-arm64-workflow-filters
when:
or:
@@ -1431,14 +1432,17 @@ jobs:
path: packages/errors/__snapshot-images__
- store-npm-logs
unit-tests-release:
verify-release-readiness:
<<: *defaults
resource_class: small
parallelism: 1
environment:
GITHUB_TOKEN: $GH_TOKEN
steps:
- restore_cached_workspace
- update_known_hosts
- run: yarn test-npm-package-release-script
- run: node ./scripts/semantic-commits/validate-binary-changelog.js
lint-types:
<<: *defaults
@@ -2373,7 +2377,7 @@ linux-x64-workflow: &linux-x64-workflow
- unit-tests:
requires:
- build
- unit-tests-release:
- verify-release-readiness:
context: test-runner:npm-release
requires:
- build
@@ -2547,7 +2551,7 @@ linux-x64-workflow: &linux-x64-workflow
- server-unit-tests
- test-kitchensink
- unit-tests
- unit-tests-release
- verify-release-readiness
- cli-visual-tests
- reporter-integration-tests
- run-app-component-tests-chrome

View File

@@ -7,12 +7,6 @@
- Closes <!-- link to the issue here, if there is one -->
### User facing changelog
<!--
Explain the change(s) for every user to read in our changelog. Examples: https://on.cypress.io/changelog
If the change is not user-facing, write "n/a".
-->
### Additional details
<!-- Examples:
- Why was this change necessary?

View File

@@ -9,13 +9,21 @@ on:
jobs:
main:
name: Lint Title
name: Semantic Pull Request
runs-on: ubuntu-latest
steps:
# use a fork of the GitHub action - we cannot pull in untrusted third party actions
# see https://github.com/cypress-io/cypress/pull/20091#discussion_r801799647
- uses: cypress-io/action-semantic-pull-request@v4
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Checkout
uses: actions/checkout@v3
with:
validateSingleCommit: true
ref: ${{ github.event.pull_request.head.ref }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
- run: npm install
working-directory: scripts/github-actions/semantic-pull-request/
- name: Lint PR Title and Cypress Changelog Entry
if: github.event_name == 'pull_request_target'
uses: actions/github-script@v4
with:
script: |
const verifyPullRequest = require('./scripts/github-actions/semantic-pull-request')
await verifyPullRequest({ context, core, github })

View File

@@ -1,20 +0,0 @@
module.exports = {
plugins: [
'@semantic-release/commit-analyzer',
'@semantic-release/release-notes-generator',
['@semantic-release/changelog', {
changelogFile: 'CHANGELOG.md',
}],
['@semantic-release/git', {
assets: [
'./CHANGELOG.md',
],
message: 'chore: release ${nextRelease.gitTag}\n\n[skip ci]',
}],
'@semantic-release/npm',
],
extends: 'semantic-release-monorepo',
branches: [
{ name: 'develop', channel: 'latest' },
],
}

View File

@@ -1,3 +1,31 @@
const { parserOpts, releaseRules } = require('./scripts/semantic-commits/change-categories')
module.exports = {
...require('./.releaserc.base'),
plugins: [
['@semantic-release/commit-analyzer', {
preset: 'angular',
parserOpts,
releaseRules,
}],
['@semantic-release/release-notes-generator',
{
preset: 'angular',
parserOpts,
}
],
['@semantic-release/changelog', {
changelogFile: 'CHANGELOG.md',
}],
['@semantic-release/git', {
assets: [
'./CHANGELOG.md',
],
message: 'chore: release ${nextRelease.gitTag}\n\n[skip ci]',
}],
'@semantic-release/npm',
],
extends: 'semantic-release-monorepo',
branches: [
{ name: 'develop', channel: 'latest' },
],
}

View File

@@ -459,7 +459,22 @@ We do not continuously deploy the Cypress binary, so `develop` contains all of t
- Break down pull requests into the smallest necessary parts to address the original issue or feature. This helps you get a timely review and helps the reviewer clearly understand which pieces of the code changes are relevant.
- When opening a PR for a specific issue already open, please name the branch you are working on using the convention `issue-[issue number]`. For example, if your PR fixes Issue #803, name your branch `issue-803`. If the PR is a larger issue, you can add more context like `issue-803-new-scrollable-area`. If there's not an associated open issue, **[create an issue](https://github.com/cypress-io/cypress/issues/new/choose)**.
- PRs can be opened before all the work is finished. In fact we encourage this! Please create a [Draft Pull Request](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-requests#draft-pull-requests) if your PR is not ready for review. [Mark the PR as **Ready for Review**](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/changing-the-stage-of-a-pull-request#marking-a-pull-request-as-ready-for-review) when you're ready for a Cypress team member to review the PR.
- Prefix the title of the Pull Request using [semantic-release](https://github.com/semantic-release/semantic-release)'s format as defined [here](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#type). For example, if your PR is fixing a bug, you should prefix the PR title with `fix:`.
- Prefix the title of the Pull Request using [semantic-release](https://github.com/semantic-release/semantic-release)'s format using one of the following definitions. Once committed to develop, this prefix will determine the appropriate 'next version' of Cypress or the corresponding npm module.
- Changes has user-facing impact:
- `breaking` - A breaking change that will require a MVB
- `dependency` - A change to a dependency that impact the user
- `deprecation` - A API deprecation notice for users
- `feat` - A new feature
- `fix` - A bug fix or regression fix.
- `misc` - a misc user-facing change, like a UI update which is not a fix or enhancement to how Cypress works
- `perf` - A code change that improves performance
- Changes that improves the codebase or system but has no user-facing impact:
- `chore` - Changes to the build process or auxiliary tools and libraries such as documentation generation
- `docs` - Documentation only changes
- `refactor` - A code change that neither fixes a bug nor adds a feature
- `revert` - Reverts a previous commit
- `test` - Adding missing or correcting existing tests
- For user-facing changes that will be released with the next Cypress version, be sure to add a changelog entry to the appropriate section in [`cli/CHANGELOG.md`](./cli/CHANGELOG.md). See [Writing the Cypress Changelog Guide](./guides/writing-the-cypress-changelog.md) for more details.
- Fill out the [Pull Request Template](./.github/PULL_REQUEST_TEMPLATE.md) completely within the body of the PR. If you feel some areas are not relevant add `N/A` as opposed to deleting those sections. PRs will not be reviewed if this template is not filled in.
- If the PR is a user facing change and you're a Cypress team member that has logged into [ZenHub](https://www.zenhub.com/) and downloaded the [ZenHub for GitHub extension](https://www.zenhub.com/extension), set the release the PR is intended to ship in from the sidebar of the PR. Follow semantic versioning to select the intended release. This is used to generate the changelog for the release. If you don't tag a PR for release, it won't be mentioned in the changelog.
![Select release for PR](https://user-images.githubusercontent.com/1271364/135139641-657015d6-2dca-42d4-a4fb-16478f61d63f.png)

68
cli/CHANGELOG.md Normal file
View File

@@ -0,0 +1,68 @@
<!-- See the ../guides/writing-the-cypress-changelog.md for details on writing the changelog. -->
## 12.4.1
_Released 01/31/2023 (PENDING)_
## 12.4.0
_Released 1/24/2023_
**Features:**
- Added official support for Vite 4 in component testing. Addresses
[#24969](https://github.com/cypress-io/cypress/issues/24969).
- Added new
[`experimentalMemoryManagement`](/guides/references/experiments#Configuration)
configuration option to improve memory management in Chromium-based browsers.
Enable this option with `experimentalMemoryManagement=true` if you have
experienced "Out of Memory" issues. Addresses
[#23391](https://github.com/cypress-io/cypress/issues/23391).
- Added new
[`experimentalSkipDomainInjection`](/guides/references/experiments#Experimental-Skip-Domain-Injection)
configuration option to disable Cypress from setting `document.domain` on
injection, allowing users to test Salesforce domains. If you believe you are
having `document.domain` issues, please see the
[`experimentalSkipDomainInjection`](/guides/references/experiments#Experimental-Skip-Domain-Injection)
guide. This config option is end-to-end only. Addresses
[#2367](https://github.com/cypress-io/cypress/issues/2367),
[#23958](https://github.com/cypress-io/cypress/issues/23958),
[#24290](https://github.com/cypress-io/cypress/issues/24290), and
[#24418](https://github.com/cypress-io/cypress/issues/24418).
- The [`.as`](/api/commands/as) command now accepts an options argument,
allowing an alias to be stored as type "query" or "static" value. This is
stored as "query" by default. Addresses
[#25173](https://github.com/cypress-io/cypress/issues/25173).
- The `cy.log()` command will now display a line break where the `\n` character
is used. Addresses
[#24964](https://github.com/cypress-io/cypress/issues/24964).
- [`component.specPattern`](/guides/references/configuration#component) now
utilizes a JSX/TSX file extension when generating a new empty spec file if
project contains at least one file with those extensions. This applies only to
component testing and is skipped if
[`component.specPattern`](/guides/references/configuration#component) has been
configured to exclude files with those extensions. Addresses
[#24495](https://github.com/cypress-io/cypress/issues/24495).
- Added support for the `data-qa` selector in the
[Selector Playground](guides/core-concepts/cypress-app#Selector-Playground) in
addition to `data-cy`, `data-test` and `data-testid`. Addresses
[#25305](https://github.com/cypress-io/cypress/issues/25305).
**Bugfixes:**
- Fixed an issue where component tests could incorrectly treat new major
versions of certain dependencies as supported. Fixes
[#25379](https://github.com/cypress-io/cypress/issues/25379).
- Fixed an issue where new lines or spaces on new lines in the Command Log were
not maintained. Fixes
[#23679](https://github.com/cypress-io/cypress/issues/23679) and
[#24964](https://github.com/cypress-io/cypress/issues/24964).
- Fixed an issue where Angular component testing projects would fail to
initialize if an unsupported browserslist entry was specified in the project
configuration. Fixes
[#25312](https://github.com/cypress-io/cypress/issues/25312).
**Misc**
- Video output link in `cypress run` mode has been added to it's own line to
make the video output link more easily clickable in the terminal. Addresses
[#23913](https://github.com/cypress-io/cypress/issues/23913).

View File

@@ -17,8 +17,10 @@ For general contributor information, check out [`CONTRIBUTING.md`](../CONTRIBUTI
* [Determining the next version of Cypress to be released](./next-version.md)
* [E2E Open Mode Testing](./e2e-open-testing.md)
* [Error handling](./error-handling.md)
* [GraphQL Subscriptions - Overview and Test Guide](./graphql-subscriptions.md)
* [Patching packages](./patch-package.md)
* [Release process](./release-process.md)
* [Testing other projects](./testing-other-projects.md)
* [Testing strategy and style guide (draft)](./testing-strategy-and-styleguide.md)
* [Writing cross-platform JavaScript](./writing-cross-platform-javascript.md)
* [Writing the Cypress Changelog](./writing-the-cypress-changelog.md)

View File

@@ -68,13 +68,7 @@ of Cypress. You can see the progress of the test projects by opening the status
In the following instructions, "X.Y.Z" is used to denote the [next version of Cypress being published](./next-version.md).
1. Confirm that every issue labeled [stage: pending release](https://github.com/cypress-io/cypress/issues?q=label%3A%22stage%3A+pending+release%22+is%3Aclosed) has a ZenHub release set. **Tip:** there is a command in [`release-automations`](https://github.com/cypress-io/release-automations)'s `issues-in-release` tool to list and check such issues. Without a ZenHub release issues will not be included in the right changelog. Also ensure that every closed issue in any obsolete releases are moved to the appropriate release in ZehHub. For example, if the open releases are 9.5.5 and 9.6.0, the current release is 9.6.0, then all closed issues marked as 9.5.5 should be moved to 9.6.0. Ensure that there are no commits on `develop` since the last release that are user facing and aren't marked with the current release.
2. If there is a new [`cypress-example-kitchensink`](https://github.com/cypress-io/cypress-example-kitchensink/releases) version, update the corresponding dependency in [`packages/example`](../packages/example) to that new version.
3. Once the `develop` branch is passing for all test projects with the new changes and the `linux-x64` binary is present at `https://cdn.cypress.io/beta/binary/X.Y.Z/linux-x64/develop-<sha>/cypress.zip`, and the `linux-x64` cypress npm package is present at `https://cdn.cypress.io/beta/npm/X.Y.Z/linux-x64/develop-<sha>/cypress.tgz`, publishing can proceed.
4. Install and test the pre-release version to make sure everything is working.
1. Install and test the pre-release version to make sure everything is working.
- Get the pre-release version that matches your system from the latest develop commit.
- Install the new version: `npm install -g <cypress.tgz path>`
- Run a quick, manual smoke test:
@@ -84,6 +78,14 @@ In the following instructions, "X.Y.Z" is used to denote the [next version of Cy
- [cypress-realworld-app](https://github.com/cypress-io/cypress-realworld-app) uses yarn and represents a typical consumer implementation.
- Optionally, do more thorough tests, for example test the new version of Cypress against the Cypress Cloud repo.
2. Confirm that every issue labeled [stage: pending release](https://github.com/cypress-io/cypress/issues?q=label%3A%22stage%3A+pending+release%22+is%3Aclosed) has a ZenHub release set. **Tip:** there is a command in [`release-automations`](https://github.com/cypress-io/release-automations)'s `issues-in-release` tool to list and check such issues. Without a ZenHub release issues will not be included in the right changelog. Also ensure that every closed issue in any obsolete releases are moved to the appropriate release in ZehHub. For example, if the open releases are 9.5.5 and 9.6.0, the current release is 9.6.0, then all closed issues marked as 9.5.5 should be moved to 9.6.0. Ensure that there are no commits on `develop` since the last release that are user facing and aren't marked with the current release.
3. Create a Release PR Bump, submit, get approvals on, and merge a new PR. This PR Should:
- Bump the Cypress `version` in [`package.json`](package.json)
- Bump the [`packages/example`](../packages/example) dependency if there is a new [`cypress-example-kitchensink`](https://github.com/cypress-io/cypress-example-kitchensink/releases) version
- Follow the writing the [Cypress Changelog release steps](./writing-the-cypress-changelog.md#release) to update the [`cli/CHANGELOG.md`](../cli/CHANGELOG.md).
4. Once the `develop` branch is passing for all test projects with the new changes and the `linux-x64` binary is present at `https://cdn.cypress.io/beta/binary/X.Y.Z/linux-x64/develop-<sha>/cypress.zip`, and the `linux-x64` cypress npm package is present at `https://cdn.cypress.io/beta/npm/X.Y.Z/linux-x64/develop-<sha>/cypress.tgz`, publishing can proceed.
5. Log into AWS SSO with `aws sso login --profile <name_of_profile>`. If you have setup your credentials under a different profile than `prod`, be sure to set the `AWS_PROFILE` environment variable to that profile name for the remaining steps. For example, if you are using `production` instead of `prod`, do `export AWS_PROFILE=production`.
6. Use the `prepare-release-artifacts` script (Mac/Linux only) to prepare the latest commit to a stable release. When you run this script, the following happens:
@@ -121,12 +123,7 @@ In the following instructions, "X.Y.Z" is used to denote the [next version of Cy
- [cypress-realworld-app](https://github.com/cypress-io/cypress-realworld-app) uses yarn and represents a typical consumer implementation.
- Optionally, do more thorough tests, for example test the new version of Cypress against the Cypress Cloud repo.
11. Create or review the release-specific documentation and changelog in [cypress-documentation](https://github.com/cypress-io/cypress-documentation). If there is not already a release-specific PR open, create one. This PR must be merged, built, and deployed before moving to the next step.
- Use [`release-automations`](https://github.com/cypress-io/release-automations)'s `issues-in-release` tool to generate a starting point for the changelog, based off of ZenHub:
```shell
cd packages/issues-in-release
yarn do:changelog --release <release label>
```
11. Review the release-specific documentation and changelog PR in [cypress-documentation](https://github.com/cypress-io/cypress-documentation). If there is not already a release-specific PR open, create one.
- Ensure the changelog is up-to-date and has the correct date.
- Merge any release-specific documentation changes into the main release PR.
- You can view the doc's [branch deploy preview](https://github.com/cypress-io/cypress-documentation/blob/master/CONTRIBUTING.md#pull-requests) by clicking 'Details' on the PR's `netlify-cypress-docs/deploy-preview` GitHub status check.
@@ -147,7 +144,7 @@ In the following instructions, "X.Y.Z" is used to denote the [next version of Cy
15. If needed, push out any updated changes to the links manifest to [`on.cypress.io`](https://github.com/cypress-io/cypress-services/tree/develop/packages/on).
16. Merge the new docker image PR created in step 13 to release the image.
16. Merge the documentation PR from step 11 and the new docker image PR created in step 12 to release the image.
17. If needed, deploy the updated [`cypress-example-kitchensink`][cypress-example-kitchensink] to `example.cypress.io` by following [these instructions under "Deployment"](../packages/example/README.md).
@@ -156,10 +153,7 @@ In the following instructions, "X.Y.Z" is used to denote the [next version of Cy
- Create a new patch release (and a new minor release, if this is a minor release) in ZenHub, and schedule them both to be completed 2 weeks from the current date.
- Move all issues that are still open from the current release to the appropriate future release.
19. Bump `version` in [`package.json`](package.json), submit, get approvals on, and merge a new PR for the change.
20. After the PR to bump the [`package.json`](package.json) version merges:
19. Once the release is complete, create a Github tag off of the release commit which bumped the version:
```shell
git checkout develop
git pull origin develop
@@ -169,17 +163,17 @@ In the following instructions, "X.Y.Z" is used to denote the [next version of Cy
git push origin vX.Y.Z
```
21. Create a new [GitHub release](https://github.com/cypress-io/cypress/releases). Choose the tag you created previously and add contents to match previous releases.
20. Create a new [GitHub release](https://github.com/cypress-io/cypress/releases). Choose the tag you created previously and add contents to match previous releases.
22. Inside of [cypress-io/release-automations][release-automations], run the following to add a comment to each GH issue that has been resolved with the new published version:
21. Inside of [cypress-io/release-automations][release-automations], run the following to add a comment to each GH issue that has been resolved with the new published version:
```shell
cd packages/issues-in-release && npm run do:comment -- --release X.Y.Z
```
23. Confirm there are no issues with the label [stage: pending release](https://github.com/cypress-io/cypress/issues?q=label%3A%22stage%3A+pending+release%22+is%3Aclosed) left
22. Confirm there are no issues with the label [stage: pending release](https://github.com/cypress-io/cypress/issues?q=label%3A%22stage%3A+pending+release%22+is%3Aclosed) left
24. Check all `cypress-test-*` and `cypress-example-*` repositories, and if there is a branch named `x.y.z` for testing the features or fixes from the newly published version `x.y.z`, update that branch to refer to the newly published NPM version in `package.json`. Then, get the changes approved and merged into that project's main branch. For projects without a `x.y.z` branch, you can go to the Renovate dependency issue and check the box next to `Update dependency cypress to X.Y.Z`. It will automatically create a PR. Once it passes, you can merge it. Try updating at least the following projects:
23. Check all `cypress-test-*` and `cypress-example-*` repositories, and if there is a branch named `x.y.z` for testing the features or fixes from the newly published version `x.y.z`, update that branch to refer to the newly published NPM version in `package.json`. Then, get the changes approved and merged into that project's main branch. For projects without a `x.y.z` branch, you can go to the Renovate dependency issue and check the box next to `Update dependency cypress to X.Y.Z`. It will automatically create a PR. Once it passes, you can merge it. Try updating at least the following projects:
- [cypress-example-todomvc](https://github.com/cypress-io/cypress-example-todomvc/issues/99)
- [cypress-example-todomvc-redux](https://github.com/cypress-io/cypress-example-todomvc-redux/issues/1)
- [cypress-realworld-app](https://github.com/cypress-io/cypress-realworld-app/issues/41)

View File

@@ -0,0 +1,61 @@
# Cypress App - Managing the Release Changelog
Cypress prefers hand tailored release notes over auto generated release notes, primarily, user experience is highly valued at Cypress. åWhile Cypress is a dependency installed via a package manager, the changelog should be more akin to other desktop products like [VS Code](https://code.visualstudio.com/updates/v1_62) or [Notion](https://www.notion.so/What-s-New-157765353f2c4705bd45474e5ba8b46c).
## When to Add an Entry
The changelog should include anything that was merged into the develop branch of the cypress repo that are user affecting changes. These include:
- `breaking` - A breaking change that will require a MVB
- `dependency` - A change to a dependency that impact the user
- `deprecation` - A API deprecation notice for users
- `feat` - A new feature
- `fix` - A bug fix or regression fix.
- `misc` - a misc user-facing change, like a UI update which is not a fix or enhancement to how Cypress works
- `perf` - A code change that improves performance
## Writing Guidelines
1. The changelog is formatted like the following. If there is not a pending changelog for the next release, add these sections.
```md
## <RELEASE_VERSION>
_Released <RELEASE_DATE> (PENDING)_
**<CHANGE_SECTION:**
- <CHANGELOG_ENTRY>
```
2. Each changelog entry is written and merged with the associated user-facing code change in [`cli/CHANGELOG.md`](../cli/CHANGELOG.md).
3. The changelog entry should be added the associated change section. The supported change sections for the changelog (that should be listed in the order below) are:
| change type (by order of impact) | change section | details |
| -- | -- | --|
| -- | Summary | A description of the overall changes. This is usually only provided for **breaking changes** or **large features**. This should be written in coordination with Cypress's marketing and match the language used around the release. It may also link to relevant blogs. [Example](https://docs.cypress.io/guides/references/changelog#7-0-0) |
| `breaking` | Breaking Changes | Link to the Migration Guide (if any) at the beginning of this section. For each one explain the change, how it affects them, and how the can mitigate the effects of the change (unless it's covered in the Migration Guide). [Example](https://docs.cypress.io/guides/references/changelog#6-0-0) |
| `deprecation` | Deprecations | Explain each deprecation and that it will be removed in a future release. [Example](https://docs.cypress.io/guides/references/changelog#6-0-0) |
| `perf` | Performance | [Example](https://docs.cypress.io/guides/references/changelog#7-2-0) |
| `feat` | Features | [Example](https://docs.cypress.io/guides/references/changelog#8-6-0) |
| `fix` | Bugfixes | [Example](https://docs.cypress.io/guides/references/changelog#9-1-0) |
| `misc` | Misc | We don't use this section as much as we used to, but perhaps there was a change that is not necessarily a feature or a bugfix, it would go here. (Like the design of the browser picker changed). [Example](https://docs.cypress.io/guides/references/changelog#6-7-0) |
| `dependency` | Dependency Updates | A list of dependencies that were updated, downgraded, or removed as well as the version it was changed from. [Example](https://docs.cypress.io/guides/references/changelog#7-2-0) |
4. You may have several changes around a feature that make sense to group. Feel free to do so to make more sense to users consuming the changelog. [Example](https://docs.cypress.io/guides/references/changelog#8-7-0)
5. Do not refer to 'we' when writing a changelog item. We want to phrase the changelog in a way that emphasizes how the user is impacted. Additionally 'we' may not have addressed the issue, an outside contributor may have.
- _Example:_ Instead of 'We fixed a situation where a cross-origin errors could incorrectly throw in Chrome' write 'Cross-origin errors will no longer incorrectly throw in Chrome in certain situations'.
6. Be as direct as possible in explaining the changes, but with enough clarity that the user understands the full impact. Users should *never* have to click on the link to the issue/PR to understand the change that happened and *absolutely never* have to look at the code to understand the change. If you cannot yourself understand the change from the Changelog entry, add more context.
7. Order the changelog items in order of impact. The most impactful features/bugfixes should be ordered first.
8. If a changelog item is a regression, the description should start with `Fixed a regression in [9.1.0](#9-1-0)` with a link to the release that introduced it.
9. For each changelog item, there should be a link to the issue(s) it addresses (or the PR that it was addressed in if there is no corresponding issue(s)). See phrasing below
* For bugfixes:
> Fixes [#12]([https://github.com/cypress-io/cypress/issues/12](https://github.com/cypress-io/cypress/issues/1234))
11. For other issues: "Addresses [#12]([https://github.com/cypress-io/cypress/issues/12](https://github.com/cypress-io/cypress/issues/1234))"
12. When no issues, but PR: "Addressed in [#12]([https://github.com/cypress-io/cypress/issues/12](https://github.com/cypress-io/cypress/issues/1234))"
13. When multiple issues: "Fixes [#12]([https://github.com/cypress-io/cypress/issues/12](https://github.com/cypress-io/cypress/issues/1234)), [#13]([https://github.com/cypress-io/cypress/issues/13](https://github.com/cypress-io/cypress/issues/1234)) and [#14]([https://github.com/cypress-io/cypress/issues/14](https://github.com/cypress-io/cypress/issues/1234))."
## Release
At the time of the release, the releaser will:
- remove the `(PENDING)` verbiage next to the perspective release date and adjust the date if needed
- ensure the Changelog is coherent
- ensure the change sections are in the correct order
- ensure that the entries are ordered by impact
Each Cypress release results in a new changelog file being added to the [`cypress-documentation`](https://github.com/cypress-io/cypress-documentation) repository to be published on the doc site. [Example pull request](https://github.com/cypress-io/cypress-documentation/pull/4141) adding changelog to the repository.

View File

@@ -1,3 +1,3 @@
module.exports = {
...require('../../.releaserc.base'),
...require('../../.releaserc'),
}

View File

@@ -1,3 +1,3 @@
module.exports = {
...require('../../.releaserc.base'),
...require('../../.releaserc'),
}

View File

@@ -1,3 +1,3 @@
module.exports = {
...require('../../.releaserc.base'),
...require('../../.releaserc'),
}

View File

@@ -1,3 +1,3 @@
module.exports = {
...require('../../.releaserc.base'),
...require('../../.releaserc'),
}

View File

@@ -1,3 +1,3 @@
module.exports = {
...require('../../.releaserc.base'),
...require('../../.releaserc'),
}

View File

@@ -1,3 +1,3 @@
module.exports = {
...require('../../.releaserc.base'),
...require('../../.releaserc'),
}

View File

@@ -1,3 +1,3 @@
module.exports = {
...require('../../.releaserc.base'),
...require('../../.releaserc'),
}

View File

@@ -1,3 +1,3 @@
module.exports = {
...require('../../.releaserc.base'),
...require('../../.releaserc'),
}

View File

@@ -1,3 +1,3 @@
module.exports = {
...require('../../.releaserc.base'),
...require('../../.releaserc'),
}

View File

@@ -8,25 +8,86 @@ const bumpCb = require('conventional-recommended-bump')
const { promisify } = require('util')
const currentVersion = require('../package.json').version
const { changeCatagories } = require('./semantic-commits/change-categories')
const bump = promisify(bumpCb)
const paths = ['packages', 'cli']
let nextVersion
const getNextVersionForPath = async (path) => {
// allow the semantic next version to be overridden by environment
if (process.env.NEXT_VERSION) {
return process.env.NEXT_VERSION
}
const { releaseType } = await bump({ preset: 'angular', path })
let commits
const whatBump = (foundCommits) => {
// semantic version bump: 0 - major, 1 - minor, 2 - patch
let level = 2
let breakings = 0
let features = 0
return semver.inc(currentVersion, releaseType || 'patch')
commits = foundCommits
foundCommits.forEach((commit) => {
if (!commit.type || !changeCatagories[commit.type]) return
if (changeCatagories[commit.type].release === 'major') {
breakings += 1
level = 0
} else if (changeCatagories[commit.type].release === 'minor') {
features += 1
if (level === 2) {
level = 1
}
}
})
return {
level,
reason: breakings > 0
? `There is ${breakings} BREAKING CHANGE and ${features} features`
: features > 0 ? `There ${features} features` : 'There are only patch changes in this release',
}
}
const { releaseType } = await bump({
whatBump,
path,
})
return {
nextVersion: semver.inc(currentVersion, releaseType || 'patch'),
commits,
}
}
const getNextVersionForBinary = async () => {
let commits = []
let nextVersion
for (const path of paths) {
const { nextVersion: pathNextVersion, commits: pathCommits } = await getNextVersionForPath(path)
if (!nextVersion || semver.gt(pathNextVersion, nextVersion)) {
nextVersion = pathNextVersion
}
commits = commits.concat(pathCommits)
}
if (!nextVersion) {
throw new Error('Unable to determine next version.')
}
return {
nextVersion,
commits,
}
}
if (require.main !== module) {
module.exports.getNextVersionForPath = getNextVersionForPath
module.exports = {
getNextVersionForBinary,
getNextVersionForPath,
}
return
}
@@ -34,17 +95,7 @@ if (require.main !== module) {
(async () => {
process.chdir(path.join(__dirname, '..'))
for (const path of paths) {
const pathNextVersion = await getNextVersionForPath(path)
if (!nextVersion || semver.gt(pathNextVersion, nextVersion)) {
nextVersion = pathNextVersion
}
}
if (!nextVersion) {
throw new Error('Unable to determine next version.')
}
const { nextVersion } = await getNextVersionForBinary()
if (process.argv.includes('--npm')) {
const cmd = `npm --no-git-tag-version version ${nextVersion}`

View File

@@ -0,0 +1,68 @@
/* eslint-disable no-console */
const { validatePrTitle } = require('./validate-pr-title')
const { validateChangelog } = require('../../semantic-commits/validate-changelog')
const { getLinkedIssues } = require('../../semantic-commits/get-linked-issues')
/**
* Semantic Pull Request:
* - check PR title
* - check for packages/cli file changes
* - If YES - verify changelog entry for user-facing commits
* - an entry must be added under the correct change section
* - an entry must include links with associated issues or a link to PR if no issues
* - ignore changelog changes even if commit doesn't include user-facing changes to allow for typo / grammar fixes
*/
async function run ({ context, core, github }) {
try {
const contextPullRequest = context.payload.pull_request
if (!contextPullRequest) {
throw new Error(
'This action can only be invoked in `pull_request_target` or `pull_request` events. Otherwise the pull request can\'t be inferred.',
)
}
// The pull request info on the context isn't up to date. When
// the user updates the title and re-runs the workflow, it would
// be outdated. Therefore fetch the pull request via the REST API
// to ensure we use the current title.
const restParameters = {
owner: contextPullRequest.base.user.login,
repo: contextPullRequest.base.repo.name,
pull_number: contextPullRequest.number,
}
const { data: pullRequest } = await github.pulls.get(restParameters)
const { type: semanticType, header } = await validatePrTitle({
github,
restParameters,
prTitle: pullRequest.title,
})
const associatedIssues = getLinkedIssues(pullRequest.body)
const { data } = await github.pulls.listFiles(restParameters)
const changedFiles = data.map((fileDetails) => fileDetails.filename)
await validateChangelog({
changedFiles,
commits: [{
commitMessage: header,
prNumber: contextPullRequest.number,
semanticType,
associatedIssues,
}],
})
} catch (error) {
core.setFailed(error.message)
}
}
// execute main function if called from command line
if (require.main === module) {
run()
}
module.exports = run

View File

@@ -0,0 +1,5 @@
{
"dependencies": {
"conventional-commits-parser": "3.2.4"
}
}

View File

@@ -0,0 +1,78 @@
const parser = require('conventional-commits-parser').sync
const { changeCatagories, parserOpts } = require('../../semantic-commits/change-categories')
const types = Object.keys(changeCatagories)
function _validateTitle (prTitle) {
const result = parser(prTitle, parserOpts)
function printAvailableTypes () {
return `Available types:\n${types
.map((type) => ` - ${type}: ${changeCatagories[type].description}`)
.join('\n')}`
}
if (!result.type) {
throw new Error(
`No release type found in pull request title "${prTitle}". Add a prefix to indicate what kind of release this pull request corresponds to. Cypress types are:/\n\n${printAvailableTypes()}`,
)
}
if (!result.subject) {
throw new Error(`No subject found in pull request title "${prTitle}".`)
}
if (!types.includes(result.type)) {
throw new Error(
`Unknown release type "${result.type}" found in pull request title "${prTitle}".
\n\n${printAvailableTypes()}`,
)
}
return result
}
async function validatePrTitle ({ github, prTitle, restParameters }) {
let result = _validateTitle(prTitle)
const commits = []
let nonMergeCommits = []
for await (const response of github.paginate.iterator(
github.pulls.listCommits,
restParameters,
)) {
commits.push(...response.data)
// GitHub does not count merge commits when deciding whether to use
// the PR title or a commit message for the squash commit message.
nonMergeCommits = commits.filter((commit) => {
return commit.parents.length < 2
})
// We only need two non-merge commits to know that the PR
// title won't be used.
if (nonMergeCommits.length >= 2) break
}
// If there is only one (non merge) commit present, GitHub will use
// that commit rather than the PR title for the title of a squash
// commit. To make sure a semantic title is used for the squash
// commit, we need to validate the commit title.
if (nonMergeCommits.length === 1) {
try {
result = _validateTitle(nonMergeCommits[0].commit.message)
} catch (error) {
throw new Error(
`Pull request has only one commit and it's not semantic; this may lead to a non-semantic commit in the base branch (see https://github.community/t/how-to-change-the-default-squash-merge-commit-message/1155). Amend the commit message to match the pull request title, or add another commit.`,
)
}
}
return result
}
module.exports = {
validatePrTitle,
_validateTitle,
}

View File

@@ -232,6 +232,7 @@ if (require.main === module) {
}
module.exports = {
getBinaryVersion,
parseSemanticReleaseOutput,
readPackageJson,
releasePackages,

View File

@@ -0,0 +1,114 @@
const changeLinkPhrases = {
default: {
hasIssue: 'Addresses',
onlyPR: 'Addressed in',
},
fix: {
hasIssue: 'Fixes',
onlyPR: 'Fixed in',
},
}
const userFacingChanges = {
breaking: {
description: 'A breaking change that will require a MVB',
section: '**Breaking Changes:**',
message: changeLinkPhrases.default,
breaking: true,
release: 'major',
},
dependency: {
description: 'A change to a dependency that impact the user',
section: '**Dependency Updates:**',
message: changeLinkPhrases.default,
release: 'patch',
},
deprecation: {
description: 'A API deprecation notice for users',
section: '**Deprecations:**',
message: changeLinkPhrases.default,
release: 'minor',
},
feat: {
description: 'A new feature',
section: '**Features:**',
message: changeLinkPhrases.default,
release: 'minor',
},
fix: {
description: 'A bug or regression fix',
section: '**Bugfixes:**',
message: changeLinkPhrases.fix,
release: 'patch',
},
misc: {
description: 'Misc user-facing changes, like a UI update, which is not a fix or enhancement to how Cypress works',
section: '**Misc:**',
message: changeLinkPhrases.default,
release: 'patch',
},
perf: {
description: 'Changes that improves performance',
section: '**Performance:**',
message: changeLinkPhrases.fix,
release: 'patch',
},
}
const changeCatagories = {
...userFacingChanges,
chore: {
description: 'Changes to the build process or auxiliary tools and libraries such as documentation generation',
release: false,
},
docs: {
description: 'Documentation only changes',
release: false,
},
refactor: {
description: 'A code change that neither fixes a bug nor adds a feature that is not user-facing',
release: false,
},
revert: {
description: 'Reverts a previous commit',
release: false,
revert: true,
},
test: {
description: 'Adding missing or correcting existing tests',
release: false,
},
}
// Used by @semantic-release/commit-analyzer to determine next version for npm packages
const releaseRules = Object.entries(changeCatagories).map(([type, attrs]) => {
return {
type,
breaking: attrs.breaking,
release: attrs.release,
revert: attrs.revert,
}
})
// https://github.com/conventional-changelog/conventional-changelog/blob/master/packages/conventional-changelog-angular/parser-opts.js
const parserOpts = {
headerPattern: /^(\w*)(?:\((.*)\))?: (.*)$/,
headerCorrespondence: [
'type',
'scope',
'subject',
],
noteKeywords: ['BREAKING CHANGE'],
revertPattern: /^(?:Revert|revert:)\s"?([\s\S]+?)"?\s*This reverts commit (\w*)\./i,
revertCorrespondence: ['header', 'hash'],
}
const migrationGuideBlurb = (version) => `Please read our Migration Guide which explains the changes in more detail and how to change your code to migrate to Cypress ${version}.`
module.exports = {
changeCatagories,
parserOpts,
releaseRules,
userFacingChanges,
migrationGuideBlurb,
}

View File

@@ -0,0 +1,149 @@
/* eslint-disable no-console */
const execa = require('execa')
const _ = require('lodash')
const { Octokit } = require('@octokit/core')
const { getNextVersionForBinary } = require('../get-next-version')
const { getLinkedIssues } = require('./get-linked-issues')
const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN })
/**
* Get the version, commit date and git sha of the latest tag published on npm.
*/
const getCurrentReleaseData = async () => {
console.log('Get Current Release Information\n')
const { stdout } = await execa('npm', ['info', 'cypress', '--json'])
const npmInfo = JSON.parse(stdout)
const latestReleaseInfo = {
version: npmInfo['dist-tags'].latest,
commitDate: npmInfo.buildInfo.commitDate,
buildSha: npmInfo.buildInfo.commitSha,
}
console.log({ latestReleaseInfo })
return latestReleaseInfo
}
/**
* Get the list of file names that have been added, deleted or changed since the git
* sha associated with the latest tag published on npm.
*
* @param {object} latestReleaseInfo - data of the latest tag published on npm
* @param {string} latestReleaseInfo.version - version of Cypress
* @param {string} latestReleaseInfo.commitDate - data of release
* @param {string} latestReleaseInfo.buildSha - git commit associated with published content
*/
const getChangedFilesSinceLastRelease = async (latestReleaseInfo) => {
const { stdout } = await execa('git', ['diff', `${latestReleaseInfo.buildSha}..`, '--name-only'])
if (!stdout) {
console.log('no files changes since last release')
return []
}
return stdout.split('\n')
}
/**
* Get the next release version given the semantic commits in the git history. Then using the commit history,
* determine which files have changed, list of PRs merged and issues resolved since the latest tag was
* published on npm. It also collects the list of commit data including the semantic type, PR and associated
* issues.
*
* @param {object} latestReleaseInfo - data of the latest tag published on npm
* @param {string} latestReleaseInfo.version - version of Cypress
* @param {string} latestReleaseInfo.commitDate - data of release
* @param {string} latestReleaseInfo.buildSha - git commit associated with published content
*/
const getReleaseData = async (latestReleaseInfo) => {
let {
nextVersion,
commits: semanticCommits,
} = await getNextVersionForBinary()
semanticCommits = _.uniqBy(semanticCommits, (commit) => commit.header)
const changedFiles = await getChangedFilesSinceLastRelease(latestReleaseInfo)
const issuesInRelease = []
const prsInRelease = []
const commits = []
await Promise.all(semanticCommits.map(async (semanticResult) => {
if (!semanticResult) return
const { type: semanticType, references } = semanticResult
if (!references.length || !references[0].issue) {
console.log('Commit does not have an associated pull request number...')
return
}
const { data: pullRequest } = await octokit.request('GET /repos/{owner}/{repo}/pulls/{pull_number}', {
owner: 'cypress-io',
repo: 'cypress',
pull_number: references[0].issue,
})
const associatedIssues = getLinkedIssues(pullRequest.body)
commits.push({
commitMessage: semanticResult.header,
semanticType,
prNumber: references[0].issue,
associatedIssues,
})
prsInRelease.push(`https://github.com/cypress-io/cypress/pulls/${references[0].issue}`)
associatedIssues.forEach((issueNumber) => {
issuesInRelease.push(`https://github.com/cypress-io/cypress/issues/${issueNumber}`)
})
}))
return {
nextVersion,
changedFiles,
commits,
issuesInRelease,
prsInRelease,
}
}
if (require.main !== module) {
module.exports = {
getCurrentReleaseData,
getReleaseData,
}
return
}
(async () => {
const latestReleaseInfo = await getCurrentReleaseData()
const {
changelogData,
issuesInRelease,
prsInRelease,
} = await getReleaseData(latestReleaseInfo)
console.log('Next release version is', changelogData.nextVersion)
console.log(`${prsInRelease.length} user-facing pull requests have merged since ${latestReleaseInfo.version} was released.`)
.prsInRelease.forEach((link) => {
console.log(' -', link)
})
console.log(`${issuesInRelease.length} user-facing issues addressed since ${latestReleaseInfo.version} was released.`)
issuesInRelease.forEach((link) => {
console.log(' -', link)
})
})()

View File

@@ -0,0 +1,27 @@
/**
* Parses the pull request body to find any associated github issues that were
* resolved when the pull request merged.
* @returns list of associated issue numbers
*/
const getLinkedIssues = (body = '') => {
// remove markdown comments
body.replace(/(<!--.*?-->)|(<!--[\S\s]+?-->)|(<!--[\S\s]*?$)/g, '')
const references = body.match(/(close[sd]?|fix(es|ed)?|resolve[s|d]?) (cypress-io\/cypress)?#\d+/gi)
if (!references) {
return []
}
const issues = []
references.forEach((issue) => {
issues.push(issue.match(/\d+/)[0])
})
return issues.filter((v, i, a) => a.indexOf(v) === i)
}
module.exports = {
getLinkedIssues,
}

View File

@@ -0,0 +1,96 @@
/* eslint-disable no-console */
const fs = require('fs')
const path = require('path')
const { userFacingChanges } = require('./change-categories')
const isValidSection = (section) => {
return Object.values(userFacingChanges).some((set) => {
return set.section === section
})
}
async function parseChangelog (pendingRelease = true) {
const changelog = fs.readFileSync(path.join(__dirname, '..', '..', 'cli', 'CHANGELOG.md'), 'utf8')
const changeLogLines = changelog.split('\n')
let parseChangelog = true
const sections = {}
let currentSection = ''
let content = []
let index = 0
let nextKnownLineBreak = 2
while (parseChangelog) {
index++
if (index >= changeLogLines.length) {
sections[currentSection] = content
parseChangelog = false
break
}
const line = changeLogLines[index]
// reached next release section
if (index > 1 && /^## \d+\.\d+\.\d+/.test(line)) {
sections[currentSection] = content
parseChangelog = false
}
if (index === 1) {
if (!/^## \d+\.\d+\.\d+/.test(line)) {
throw new Error(`Expected line number ${index} to include "## x.x.x"`)
}
sections['version'] = line
} else if (index === 3) {
nextKnownLineBreak = index + 1
if (pendingRelease && !/_Released \d+\/\d+\/\d+ \(PENDING\)_/.test(line)) {
throw new Error(`Expected line number ${index} to include "_Released xx/xx/xxxx (PENDING)_"`)
} else if (!pendingRelease && !/_Released \d+\/\d+\/\d+__/.test(line)) {
throw new Error(`Expected line number ${index} to include "_Released xx/xx/xxxx_"`)
}
sections['releaseDate'] = line
} else if (index === nextKnownLineBreak) {
if (line !== '') {
throw new Error(`Expected line number ${index} to be a line break`)
}
} else {
const result = /\*\*([A-Z])\w+:\*\*/.exec(line)
if (result) {
const section = result[0]
if (!isValidSection(section)) {
throw new Error(`Expected line number ${index} to be a valid section header. Received ${section}. Expected one of ...`)
}
if (result === currentSection || sections[section]) {
throw new Error(`Duplicate section header of "${section}" on line number ${index}. Condense change content under a single section header.`)
}
if (currentSection !== '') {
sections[currentSection] = content
}
content = []
currentSection = section
nextKnownLineBreak = index + 1
} else {
content.push(line)
}
}
}
return sections
}
if (require.main !== module) {
module.exports.parseChangelog = parseChangelog
return
}
(async () => {
await parseChangelog()
})()

View File

@@ -0,0 +1,46 @@
/* eslint-disable no-console */
const { getBinaryVersion } = require('../npm-release')
const { validateChangelog } = require('./validate-changelog')
const { getCurrentReleaseData, getReleaseData } = require('./get-binary-release-data')
const changelog = async () => {
const latestReleaseInfo = await getCurrentReleaseData()
if (process.env.CIRCLECI) {
const checkedInBinaryVersion = await getBinaryVersion()
console.log({ checkedInBinaryVersion })
const hasVersionBump = checkedInBinaryVersion !== latestReleaseInfo.version
if (process.env.CIRCLE_BRANCH !== 'develop' || !/^release\/\d+\.\d+\.\d+$/.test(process.env.CIRCLE_BRANCH) || !hasVersionBump) {
console.log('Only verify the entire changelog for develop, a release branch or any branch that bumped to the Cypress version in the package.json.')
return
}
}
const {
nextVersion,
changedFiles,
commits,
} = await getReleaseData(latestReleaseInfo)
console.log({ nextVersion })
return validateChangelog({
nextVersion,
changedFiles,
commits,
})
}
if (require.main !== module) {
module.exports.changelog = changelog
return
}
(async () => {
await changelog()
})()

View File

@@ -0,0 +1,174 @@
/* eslint-disable no-console */
const { userFacingChanges } = require('./change-categories')
const { parseChangelog } = require('./parse-changelog')
// whether or not the semantic type is a user-facing semantic-type
const hasUserFacingChange = (type) => Object.keys(userFacingChanges).includes(type)
/**
* Formats the resolved message that is appended to the changelog entry to indicate what
* issues where addressed by a given change. If no issues are addressed, it references the
* pull request which made the change.
*/
function _getResolvedMessage (semanticType, prNumber, associatedIssues = []) {
if (associatedIssues.length) {
const issueMessage = userFacingChanges[semanticType].message.hasIssue
const links = associatedIssues.sort((a, b) => a - b)
.map((issueNumber) => {
return `[#${issueNumber}](https://github.com/cypress-io/cypress/issues/${issueNumber})`
})
// one issue: [#num]
// two issues: [#num] and [#num]
// two+ issues: [#num], [#num] and [#num]
const linkMessage = [links.slice(0, -1).join(', '), links.slice(-1)[0]].join(links.length < 2 ? '' : ' and ')
return `${issueMessage} ${linkMessage}.`
}
const prMessage = userFacingChanges[semanticType].message.onlyPR
return `${prMessage} [#${prNumber}](https://github.com/cypress-io/cypress/pull/${prNumber}).`
}
/**
* Helper to format an example of what the changelog entry might look like for a given commit.
*/
function _printChangeLogExample (semanticType, prNumber, associatedIssues = []) {
const resolveMessage = _getResolvedMessage(semanticType, prNumber, associatedIssues)
return `${userFacingChanges[semanticType].section}\n - <Insert change details>. ${resolveMessage}`
}
/**
* Ensures the changelog entry was added to the correct changelog section given it's semantic commit type
* and that it includes the correct reference(s) to the issue(s) or pull request the commit addressed.
*/
function _validateEntry (changelog, { commitMessage, prNumber, semanticType, associatedIssues }) {
if (!hasUserFacingChange(semanticType)) {
return
}
const expectedSection = userFacingChanges[semanticType].section
let missingExpectedSection = false
let sectionEntryFoundIn = ''
const resolveMessage = _getResolvedMessage(semanticType, prNumber, associatedIssues)
const hasMatchingEntry = Object.entries(userFacingChanges).some(([type, { section }]) => {
const sectionDetails = changelog[section]
if (!sectionDetails) {
missingExpectedSection = semanticType === type
return false
}
const hasMatchingEntry = sectionDetails.some((detail) => detail.includes(resolveMessage))
if (hasMatchingEntry) {
sectionEntryFoundIn = section
}
return hasMatchingEntry
})
if (missingExpectedSection) {
return `The changelog does not include the ${expectedSection} section. Given the pull request title provided, this section should be included in the changelog. If the changelog section is correct, please correct the pull request title to correctly reflect the change being made.`
}
if (!hasMatchingEntry) {
if (associatedIssues && associatedIssues.length) {
return `The changelog entry does not include the linked issues that this pull request resolves. Please update your entry for '${commitMessage}' to include:\n\n${resolveMessage}`
}
return `The changelog entry does not include the pull request link. Please update your entry for '${commitMessage}' to include:\n\n${resolveMessage}`
}
if (hasMatchingEntry && sectionEntryFoundIn !== expectedSection) {
return `Found the changelog entry in the wrong section. Expected the entry to be under the ${expectedSection} section, but found it in the ${sectionEntryFoundIn} section. Please move your entry to the correct changelog section.`
}
return
}
const _handleErrors = (errors) => {
errors.forEach((err) => {
console.log(err)
console.log()
})
throw new Error('There was one or more errors when validating the changelog. See above for details.')
}
/**
* Determines if the Cypress changelog has the correct next version and changelog entires given the provided
* list of commits.
*/
async function validateChangelog ({ changedFiles, nextVersion, commits }) {
const hasUserFacingCommits = commits.some(({ semanticType }) => hasUserFacingChange(semanticType))
if (!hasUserFacingCommits) {
console.log('Does not contain any user-facing changes that impacts the next Cypress release.')
return []
}
const hasChangeLogUpdate = changedFiles.includes('cli/CHANGELOG.md')
const binaryFiles = changedFiles.filter((filename) => {
return /^(cli|packages)/.test(filename) && filename !== 'cli/CHANGELOG.md'
})
let errors = []
if (binaryFiles.length === 0) {
console.log('Does not contain changes that impacts the next Cypress release.')
return []
}
if (!hasChangeLogUpdate) {
errors.push(`A changelog entry was not found in cli/CHANGELOG.md.`)
if (commits.length === 1) {
errors.push(`Please add a changelog entry that describes the changes. Include this entry under the section:/\n\n${_printChangeLogExample(commits[0].semanticType, commits[0].prNumber, commits[0].associatedIssues)}`)
return _handleErrors(errors)
}
}
const changelog = await parseChangelog()
if (nextVersion && !changelog.version === `## ${nextVersion}`) {
errors.push(`The changelog version does not contain the next Cypress version of ${nextVersion}. If the changelog version is correct, please correct the pull request title to correctly reflect the change being made.`)
}
commits.forEach(({ commitMessage, semanticType, prNumber, associatedIssues }) => {
if (!Object.keys(userFacingChanges).includes(semanticType)) {
return
}
if (!hasChangeLogUpdate) {
_printChangeLogExample(semanticType, prNumber, associatedIssues)
}
const errMessage = _validateEntry(changelog, { commitMessage, semanticType, prNumber, associatedIssues })
if (errMessage) {
errors.push(errMessage)
}
})
if (errors.length) {
_handleErrors(errors)
}
console.log('It appears at a high-level your changelog entry is correct! The remaining validation is left to the pull request reviewers.')
}
module.exports = {
validateChangelog,
_validateEntry,
_getResolvedMessage,
}

View File

@@ -0,0 +1,166 @@
const { expect } = require('chai')
const sinon = require('sinon')
const { validatePrTitle, _validateTitle } = require('../../../github-actions/semantic-pull-request/validate-pr-title')
describe('semantic-pull-request/validate-pr-title', () => {
context('validatePrTitle', () => {
const restParameters = {
owner: 'cypress-io',
repo: 'cypress',
pull_number: 52,
}
describe('more than one commit', () => {
it('only validates pr title', async () => {
const commits = [
{ parents: ['dev'] },
{ parents: ['dev'] },
]
const myAsyncIterable = {
async *[Symbol.asyncIterator] () {
yield { data: commits }
},
}
const github = {
paginate: {
iterator: sinon.stub().returns(myAsyncIterable),
},
pulls: {},
}
const prTitle = 'fix: issue with server'
const semanticResult = await validatePrTitle({
github,
prTitle,
restParameters,
})
expect(semanticResult.type).to.eq('fix')
})
})
describe('one commit', () => {
it('only validates pr title and commit message', async () => {
const commits = [
{ parents: ['dev'], commit: { message: 'fix: issue with server and add test' } },
]
const myAsyncIterable = {
async *[Symbol.asyncIterator] () {
yield { data: commits }
},
}
const github = {
paginate: {
iterator: sinon.stub().returns(myAsyncIterable),
},
pulls: {},
}
const prTitle = 'fix: issue with server'
const semanticResult = await validatePrTitle({
github,
prTitle,
restParameters,
})
expect(semanticResult.type).to.eq('fix')
})
it('throws when commit message does not follow semantics', async () => {
const commits = [
{ parents: ['dev'], commit: { message: 'fix issue with server and add test' } },
]
const myAsyncIterable = {
async *[Symbol.asyncIterator] () {
yield { data: commits }
},
}
const github = {
paginate: {
iterator: sinon.stub().returns(myAsyncIterable),
},
pulls: {},
}
const prTitle = 'fix: issue with server'
return validatePrTitle({
github,
prTitle,
restParameters,
}).catch((err) => {
expect(err.message).to.include('Pull request has only one commit and it\'s not semantic')
})
})
})
})
context('_validateTitle', () => {
it('allows valid PR titles', () => {
[
{
type: 'breaking',
title: 'breaking: change behavior',
},
{
type: 'fix',
title: 'fix: Fix bug',
},
{
type: 'perf',
title: 'perf: make things faster',
},
{
type: 'chore',
title: 'chore: do something',
},
{
type: 'refactor',
title: 'refactor: Internal cleanup',
},
].forEach(({ title, type }) => {
expect(_validateTitle(title)).to.contain({ type })
})
})
it('throws for PR titles without a type', () => {
expect(() => _validateTitle('Fix bug')).to.throw(
'No release type found in pull request title "Fix bug". Add a prefix to indicate what kind of release this pull request corresponds to.',
)
})
it('throws for PR titles with only a type', () => {
expect(() => _validateTitle('fix:')).to.throw(
'No release type found in pull request title "fix:".',
)
})
it('throws for PR titles without a subject', () => {
expect(() => _validateTitle('fix: ')).to.throw(
'No subject found in pull request title "fix: ".',
)
})
it('throws for PR titles with an unknown type', () => {
expect(() => _validateTitle('foo: Bar')).to.throw(
'Unknown release type "foo" found in pull request title "foo: Bar".',
)
})
describe('defined scopes', () => {
it('allows a missing scope by default', () => {
_validateTitle('fix: Bar')
})
it('allows all scopes by default', () => {
_validateTitle('fix(core): Bar')
})
})
})
})

View File

@@ -0,0 +1,327 @@
/* eslint-disable no-console */
const { expect, use } = require('chai')
const sinonChai = require('sinon-chai')
const sinon = require('sinon')
const fs = require('fs')
const { validateChangelog, _validateEntry, _getResolvedMessage } = require('../../semantic-commits/validate-changelog')
use(sinonChai)
describe('semantic-pull-request/validate-changelog', () => {
context('_getResolvedMessage', () => {
it('returned pr link', () => {
const message = _getResolvedMessage('feat', 52, [])
expect(message).to.contain('Addressed in [#52](https://github.com/cypress-io/cypress/pull/52).')
})
it('returns linked issue', () => {
const message = _getResolvedMessage('feat', 52, [39])
expect(message).to.contain('Addresses [#39](https://github.com/cypress-io/cypress/issues/39).')
})
it('returns all linked issues', () => {
let message = _getResolvedMessage('feat', 52, [39, 20])
expect(message).to.contain('Addresses [#20](https://github.com/cypress-io/cypress/issues/20) and [#39](https://github.com/cypress-io/cypress/issues/39).')
message = _getResolvedMessage('feat', 52, [39, 20, 30])
expect(message).to.contain('Addresses [#20](https://github.com/cypress-io/cypress/issues/20), [#30](https://github.com/cypress-io/cypress/issues/30) and [#39](https://github.com/cypress-io/cypress/issues/39).')
})
})
context('_validateEntry', () => {
it('verifies changelog entry has been included', () => {
const errMessage = _validateEntry(
{ '**Performance:**': ['- Fixed in [#77](https://github.com/cypress-io/cypress/pull/77).'] },
{
commitMessage: 'perf: fix an issue (#77)',
semanticType: 'perf',
prNumber: 77,
associatedIssues: [],
},
)
expect(errMessage).to.undefined
})
it('returns an error when entry does not correct change section', () => {
const errMessage = _validateEntry(
{ '**Bugfixes:**': ['- Fixed in [#75](https://github.com/cypress-io/cypress/issues/75).'] },
{
commitMessage: 'perf: do something faster (#77)',
semanticType: 'perf',
prNumber: 77,
associatedIssues: ['75'],
},
)
expect(errMessage).to.contain('The changelog does not include the **Performance:** section.')
})
it('returns an error when entry does not include associated issue links', () => {
const errMessage = _validateEntry(
{ '**Performance:**': ['does another thing not related'] },
{
commitMessage: 'perf: do something faster (#77)',
semanticType: 'perf',
prNumber: 77,
associatedIssues: ['75'],
},
)
expect(errMessage).to.contain('The changelog entry does not include the linked issues that this pull request resolves.')
})
it('returns an error when entry does not include pull request link', () => {
const errMessage = _validateEntry(
{ '**Performance:**': ['does another thing not related'] },
{
commitMessage: 'perf: do something faster (#77)',
semanticType: 'perf',
prNumber: 77,
},
)
expect(errMessage).to.contain('The changelog entry does not include the pull request link.')
})
it('returns multiple error when entry does not correct section or include pull request link', () => {
const errMessage = _validateEntry(
{ '**Features:**': ['does another cool'] },
{
commitMessage: 'perf: do something faster (#77)',
semanticType: 'perf',
prNumber: 77,
},
)
expect(errMessage).to.contain('The changelog does not include the **Performance:** section.')
})
})
context('validateChangelog', () => {
beforeEach(function () {
sinon.spy(console, 'log')
sinon.stub(fs, 'readFileSync')
})
afterEach(function () {
console.log.restore()
fs.readFileSync.restore()
})
it('verifies changelog entry has been included', async () => {
const changedFiles = [
'packages/driver/lib/index.js',
'cli/CHANGELOG.md',
]
fs.readFileSync.returns(`
## 120.2.0
_Released 01/17/2033 (PENDING)_
**Performance:**
- Fixed in [#77](https://github.com/cypress-io/cypress/pull/77).`)
await validateChangelog({
changedFiles,
commits: [{
prNumber: 77,
semanticType: 'perf',
}],
})
expect(console.log).to.be.calledWith('It appears at a high-level your changelog entry is correct! The remaining validation is left to the pull request reviewers.')
})
describe('ignores validation', () => {
it('when commit has cli or binary file changes that are not user facing', async () => {
const changedFiles = [
'packages/types/src/index.tsx',
]
await validateChangelog({
changedFiles,
commits: [{
prNumber: 77,
semanticType: 'chore',
associatedIssues: ['75'],
}],
})
expect(console.log).to.be.calledWith('Does not contain any user-facing changes that impacts the next Cypress release.')
})
it('when commit does not include cli or binary file changes', async () => {
const changedFiles = [
'npm/grep/lib/index.js',
]
await validateChangelog({
changedFiles,
commits: [{
prNumber: 77,
semanticType: 'feat',
associatedIssues: ['75'],
}],
})
expect(console.log).to.be.calledWith('Does not contain changes that impacts the next Cypress release.')
})
})
describe('throws an error when', () => {
it('entry is missing', async () => {
const changedFiles = [
'packages/driver/lib/index.js',
]
fs.readFileSync.returns(`
## 120.2.0
_Released 01/17/2033 (PENDING)_
`)
return validateChangelog({
changedFiles,
commits: [{
commitMessage: 'feat: do something new (#77)',
prNumber: 77,
semanticType: 'feat',
associatedIssues: ['75'],
}],
}).catch((err) => {
expect(console.log).to.be.calledWith('A changelog entry was not found in cli/CHANGELOG.md.')
expect(err.message).to.contain('There was one or more errors when validating the changelog. See above for details.')
})
})
it('entry does not include correct change section', async () => {
const changedFiles = [
'packages/driver/lib/index.js',
'cli/CHANGELOG.md',
]
fs.readFileSync.returns(`
## 120.2.0
_Released 01/17/2033 (PENDING)_
**Features:**
- Addresses [#75](https://github.com/cypress-io/cypress/issues/75).`)
return validateChangelog({
changedFiles,
commits: [{
commitMessage: 'perf: do something faster (#77)',
prNumber: 77,
semanticType: 'perf',
associatedIssues: ['75'],
}],
}).catch((err) => {
expect(err.message).to.contain('There was one or more errors when validating the changelog. See above for details.')
expect(console.log.firstCall.args[0]).to.contain('The changelog does not include the **Performance:** section.')
})
})
it('entry added to wrong change section', async () => {
const changedFiles = [
'packages/driver/lib/index.js',
'cli/CHANGELOG.md',
]
fs.readFileSync.returns(`
## 120.2.0
_Released 01/17/2033 (PENDING)_
**Performance:**
- Some other update already added & vetted. Addresses [#32](https://github.com/cypress-io/cypress/issues/32).
**Features:**
- Fixes [#75](https://github.com/cypress-io/cypress/issues/75).`)
return validateChangelog({
changedFiles,
commits: [{
commitMessage: 'perf: do something faster (#77)',
prNumber: 77,
semanticType: 'perf',
associatedIssues: ['75'],
}],
}).catch((err) => {
expect(err.message).to.contain('There was one or more errors when validating the changelog. See above for details.')
expect(console.log.firstCall.args[0]).to.contain('Found the changelog entry in the wrong section.')
})
})
it('entry does not include associated issue links', async () => {
const changedFiles = [
'packages/driver/lib/index.js',
'cli/CHANGELOG.md',
]
fs.readFileSync.returns(`
## 120.2.0
_Released 01/17/2033 (PENDING)_
**Performance:**
- comment without link.`)
return validateChangelog({
changedFiles,
commits: [{
commitMessage: 'perf: do something faster (#77)',
prNumber: 77,
semanticType: 'perf',
associatedIssues: ['75'],
}],
}).catch((err) => {
expect(err.message).to.contain('There was one or more errors when validating the changelog. See above for details.')
expect(console.log.firstCall.args[0]).to.contain('The changelog entry does not include the linked issues that this pull request resolves.')
})
})
it('entry does not include pull request link', async () => {
const changedFiles = [
'packages/driver/lib/index.js',
'cli/CHANGELOG.md',
]
fs.readFileSync.returns(`
## 120.2.0
_Released 01/17/2033 (PENDING)_
**Performance:**
- comment without link.`)
return validateChangelog({
changedFiles,
commits: [{
commitMessage: 'perf: do something faster (#77)',
prNumber: 77,
semanticType: 'perf',
}],
})
.catch((err) => {
expect(err.message).to.contain('There was one or more errors when validating the changelog. See above for details.')
expect(console.log.firstCall.args[0]).to.contain('The changelog entry does not include the pull request link.')
})
})
})
})
})

View File

@@ -0,0 +1,65 @@
const { expect, use } = require('chai')
const sinonChai = require('sinon-chai')
const { getIssueNumbers } = require('../../semantic-commits/get-linked-issues')
use(sinonChai)
describe('semantic-commits/get-linked-issues', () => {
it('returns single issue link', () => {
const issues = getIssueNumbers(`
<!-- comment ->
- Closes #23
summary of changes see in #458
`)
expect(issues).to.deep.eq(['23'])
})
it('returns issue links for all linking keywords', () => {
const issues = getIssueNumbers(`
<!-- comment ->
- Close #23
- Closes #24
- Closed #25
- fix cypress-io/cypress#33, fixed cypress-io/cypress#34
- fixes cypress-io/cypress#35
- resolves #44
- Resolved #45
- Resolves #46
- addresses #77 <-- not a valid linking word
summary of changes
`)
expect(issues).to.deep.eq(['23', '24', '25', '33', '34', '35', '44', '45', '46'])
})
it('only counts an issue once', () => {
const body = `
- closes #44
- closes #44
`
const issues = getIssueNumbers(body)
expect(issues).to.deep.eq(['44'])
})
it('does not return non-local issue numbers', () => {
const body = `
fixes cypress-io/cypress#123 which is a local issue
and this is issue in another repo foo/bar#101
`
const issues = getIssueNumbers(body)
expect(issues).to.deep.eq(['123'])
})
it('returns empty list when no issues found', () => {
const issues = getIssueNumbers(`
<!-- comment ->
summary of changes
`)
expect(issues).to.deep.eq([])
})
})

View File

@@ -39,7 +39,7 @@ circleCiRootEvent.add({
// Therefore, we have each honeycomb event await this promise
// before sending itself.
let asyncInfo = Promise.all([getNextVersionForPath(path.resolve(__dirname, '../../packages')), commitInfo()])
.then(([nextVersion, commitInformation]) => {
.then(([{ nextVersion }, commitInformation]) => {
const ciInformation = ciProvider.commitParams() || {}
return {
@@ -62,6 +62,7 @@ class HoneycombReporter {
return
}
// eslint-disable-next-line
console.log(chalk.green('Reporting to honeycomb'))
runner.on('suite', (suite) => {