├── .eslintignore ├── .eslintrc.json ├── .github └── workflows │ ├── ci.yaml │ └── codeql-analysis.yml ├── .gitignore ├── .prettierrc.json ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── bin └── cli.js ├── lib ├── RescopedStream.js ├── blork.js ├── cleanPath.js ├── createInlinePluginCreator.js ├── getCommitsFiltered.js ├── getConfig.js ├── getConfigMultiSemrel.js ├── getConfigSemantic.js ├── getManifest.js ├── git.js ├── logger.js ├── multiSemanticRelease.js ├── recognizeFormat.js ├── updateDeps.js └── utils.js ├── package.json ├── test ├── bin │ └── cli.test.js ├── fixtures │ ├── badDepsPackage.json │ ├── badDevDepsPackage.json │ ├── badNamePackage.json │ ├── badPeerDepsPackage.json │ ├── badYarnWorkspaces │ │ └── package.json │ ├── boltWorkspaces │ │ ├── package.json │ │ └── packages │ │ │ ├── a │ │ │ └── package.json │ │ │ ├── b │ │ │ └── package.json │ │ │ ├── c │ │ │ ├── .releaserc.json │ │ │ └── package.json │ │ │ └── d │ │ │ └── package.json │ ├── boltWorkspacesIgnore │ │ ├── package.json │ │ └── packages │ │ │ ├── a │ │ │ └── package.json │ │ │ ├── b │ │ │ └── package.json │ │ │ ├── c │ │ │ ├── .releaserc.json │ │ │ └── package.json │ │ │ └── d │ │ │ └── package.json │ ├── boltWorkspacesUndefined │ │ ├── package.json │ │ └── packages │ │ │ ├── a │ │ │ └── package.json │ │ │ ├── b │ │ │ └── package.json │ │ │ ├── c │ │ │ ├── .releaserc.json │ │ │ └── package.json │ │ │ └── d │ │ │ └── package.json │ ├── emptyYarnWorkspaces │ │ └── package.json │ ├── invalidPackage.json │ ├── numberPackage.json │ ├── pnpmWorkspace │ │ ├── package.json │ │ ├── packages │ │ │ ├── a │ │ │ │ └── package.json │ │ │ ├── b │ │ │ │ └── package.json │ │ │ ├── c │ │ │ │ ├── .releaserc.json │ │ │ │ └── package.json │ │ │ └── d │ │ │ │ └── package.json │ │ └── pnpm-workspace.yaml │ ├── pnpmWorkspaceIgnore │ │ ├── package.json │ │ ├── packages │ │ │ ├── a │ │ │ │ └── package.json │ │ │ ├── b │ │ │ │ └── package.json │ │ │ ├── c │ │ │ │ ├── .releaserc.json │ │ │ │ └── package.json │ │ │ └── d │ │ │ │ └── package.json │ │ └── pnpm-workspace.yaml │ ├── pnpmWorkspaceUndefined │ │ ├── package.json │ │ ├── packages │ │ │ ├── a │ │ │ │ └── package.json │ │ │ ├── b │ │ │ │ └── package.json │ │ │ ├── c │ │ │ │ ├── .releaserc.json │ │ │ │ └── package.json │ │ │ └── d │ │ │ │ └── package.json │ │ └── pnpm-workspace.yaml │ ├── undefinedYarnWorkspaces │ │ └── package.json │ ├── yarnWorkspaces │ │ ├── package.json │ │ └── packages │ │ │ ├── a │ │ │ └── package.json │ │ │ ├── b │ │ │ └── package.json │ │ │ ├── c │ │ │ ├── .releaserc.json │ │ │ └── package.json │ │ │ └── d │ │ │ └── package.json │ ├── yarnWorkspaces2Packages │ │ ├── package.json │ │ └── packages │ │ │ ├── c │ │ │ ├── .releaserc.json │ │ │ └── package.json │ │ │ └── d │ │ │ └── package.json │ ├── yarnWorkspacesAcyclic │ │ ├── package.json │ │ └── packages │ │ │ ├── a │ │ │ └── package.json │ │ │ ├── b │ │ │ └── package.json │ │ │ ├── c │ │ │ ├── .releaserc.json │ │ │ └── package.json │ │ │ └── d │ │ │ └── package.json │ ├── yarnWorkspacesConfig │ │ ├── package.json │ │ └── packages │ │ │ ├── a │ │ │ └── package.json │ │ │ ├── b │ │ │ └── package.json │ │ │ ├── c │ │ │ ├── .releaserc.json │ │ │ └── package.json │ │ │ └── d │ │ │ └── package.json │ ├── yarnWorkspacesConfigExtends │ │ ├── config.js │ │ ├── package.json │ │ └── packages │ │ │ ├── a │ │ │ └── package.json │ │ │ ├── b │ │ │ └── package.json │ │ │ ├── c │ │ │ ├── .releaserc.json │ │ │ └── package.json │ │ │ └── d │ │ │ └── package.json │ ├── yarnWorkspacesIgnore │ │ ├── package.json │ │ └── packages │ │ │ ├── a │ │ │ └── package.json │ │ │ ├── b │ │ │ └── package.json │ │ │ ├── c │ │ │ ├── .releaserc.json │ │ │ └── package.json │ │ │ └── d │ │ │ └── package.json │ ├── yarnWorkspacesIgnoreSplit │ │ ├── package.json │ │ └── packages │ │ │ ├── a │ │ │ └── package.json │ │ │ ├── b │ │ │ └── package.json │ │ │ ├── c │ │ │ ├── .releaserc.json │ │ │ └── package.json │ │ │ └── d │ │ │ └── package.json │ └── yarnWorkspacesPackages │ │ ├── package.json │ │ └── packages │ │ ├── a │ │ └── package.json │ │ ├── b │ │ └── package.json │ │ ├── c │ │ ├── .releaserc.json │ │ └── package.json │ │ └── d │ │ └── package.json ├── helpers │ ├── file.js │ └── git.js └── lib │ ├── cleanPath.test.js │ ├── getCommitsFiltered.test.js │ ├── getConfigMultiSemrel.test.js │ ├── getPackagePaths.test.js │ ├── git.test.js │ ├── multiSemanticRelease.test.js │ ├── recognizeFormat.test.js │ ├── resolveReleaseType.test.js │ ├── updateDeps.test.js │ └── utils.test.js └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /coverage/ -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "sourceType": "module", 4 | "ecmaVersion": 2020, 5 | "experimentalObjectRestSpread": true 6 | }, 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:prettier/recommended" 10 | ], 11 | "env": { 12 | "es6": true, 13 | "node": true, 14 | "jest": true 15 | }, 16 | "rules": { 17 | "eqeqeq": 2, 18 | "no-alert": 2, 19 | "no-caller": 2, 20 | "no-eval": 2, 21 | "no-extend-native": 2, 22 | "no-floating-decimal": 2, 23 | "no-implicit-globals": 2, 24 | "no-labels": 2, 25 | "no-loop-func": 2, 26 | "no-new-require": 2, 27 | "no-path-concat": 2, 28 | "no-useless-rename": 2, 29 | "no-var": 2, 30 | "no-shadow": 2, 31 | "prefer-const": 2, 32 | "prefer-spread": 2, 33 | "strict": 2, 34 | "valid-jsdoc": 2, 35 | "global-require": 0, 36 | "no-console": 0, 37 | "no-unused-vars": 0, 38 | "require-atomic-updates": 0, 39 | "no-prototype-builtins": 0 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | # This is a Github Workflow that runs tests on any push or pull request. 2 | # If the tests pass and this is a push to the master branch it also runs Semantic Release. 3 | name: CI 4 | on: [push, pull_request] 5 | jobs: 6 | push: 7 | if: github.event_name == 'push' 8 | runs-on: ubuntu-22.04 9 | steps: 10 | - uses: actions/checkout@v3 11 | 12 | - uses: actions/setup-node@v3 13 | with: 14 | node-version: 18 15 | - run: yarn 16 | 17 | - name: Unit test only 18 | run: yarn test 19 | 20 | - name: Codeclimate 21 | if: github.ref == 'refs/heads/master' 22 | uses: paambaati/codeclimate-action@v8.0.0 23 | env: 24 | CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }} 25 | with: 26 | coverageLocations: | 27 | ${{github.workspace}}/coverage/lcov.info:lcov 28 | 29 | - name: Release 30 | if: github.ref == 'refs/heads/master' 31 | env: 32 | GH_TOKEN: ${{ secrets.GH_TOKEN }} 33 | GH_USER: ${{ secrets.GH_USER }} 34 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 35 | GIT_AUTHOR_EMAIL: ${{ secrets.GIT_AUTHOR_EMAIL }} 36 | GIT_AUTHOR_NAME: ${{ secrets.GIT_AUTHOR_EMAIL }} 37 | GIT_COMMITTER_EMAIL: ${{ secrets.GIT_COMMITTER_EMAIL }} 38 | GIT_COMMITTER_NAME: ${{ secrets.GIT_COMMITTER_NAME }} 39 | run: npx -p @qiwi/semrel-toolkit semrel 40 | 41 | pr: 42 | if: github.event_name == 'pull_request' 43 | strategy: 44 | matrix: 45 | os: [ ubuntu-22.04 ] 46 | node-version: [ 18, 20 ] 47 | name: Test (Node v${{ matrix.node-version }}, OS ${{ matrix.os }}) 48 | runs-on: ${{ matrix.os }} 49 | steps: 50 | - uses: actions/checkout@v3 51 | 52 | - uses: actions/setup-node@v3 53 | with: 54 | node-version: ${{ matrix.node-version }} 55 | - run: yarn 56 | 57 | - name: Run tests 58 | run: yarn test 59 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '26 14 * * 4' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | 28 | strategy: 29 | fail-fast: false 30 | matrix: 31 | language: [ 'javascript' ] 32 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 33 | # Learn more: 34 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 35 | 36 | steps: 37 | - name: Checkout repository 38 | uses: actions/checkout@v2 39 | 40 | # Initializes the CodeQL tools for scanning. 41 | - name: Initialize CodeQL 42 | uses: github/codeql-action/init@v1 43 | with: 44 | languages: ${{ matrix.language }} 45 | # If you wish to specify custom queries, you can do so here or in a config file. 46 | # By default, queries listed here will override any specified in a config file. 47 | # Prefix the list here with "+" to use these queries and those in the config file. 48 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 49 | 50 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 51 | # If this step fails, then you should remove it and run the build manually (see below) 52 | - name: Autobuild 53 | uses: github/codeql-action/autobuild@v1 54 | 55 | # ℹ️ Command-line programs to run using the OS shell. 56 | # 📚 https://git.io/JvXDl 57 | 58 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 59 | # and modify them (or add more) to build your code if your project 60 | # uses a compiled language 61 | 62 | #- run: | 63 | # make bootstrap 64 | # make release 65 | 66 | - name: Perform CodeQL Analysis 67 | uses: github/codeql-action/analyze@v1 68 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/node_modules/** 2 | /coverage/ 3 | *.log 4 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 4, 3 | "useTabs": true, 4 | "printWidth": 120, 5 | "parser": "babel", 6 | "proseWrap": "never" 7 | } 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [7.1.2](https://github.com/qiwi/multi-semantic-release/compare/v7.1.1...v7.1.2) (2024-07-28) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * bump release type by dependent if deps.release is inherit ([#100](https://github.com/qiwi/multi-semantic-release/issues/100)) ([d44f324](https://github.com/qiwi/multi-semantic-release/commit/d44f32485c841b66c71e1e8e03cd59e5e51ec32b)) 7 | 8 | ## [7.1.1](https://github.com/qiwi/multi-semantic-release/compare/v7.1.0...v7.1.1) (2023-10-23) 9 | 10 | 11 | ### Performance Improvements 12 | 13 | * tech release to publish [#99](https://github.com/qiwi/multi-semantic-release/issues/99) ([34328e1](https://github.com/qiwi/multi-semantic-release/commit/34328e124616aae8ec506af02419a071357e6b4a)) 14 | 15 | # [7.1.0](https://github.com/qiwi/multi-semantic-release/compare/v7.0.0...v7.1.0) (2023-10-16) 16 | 17 | 18 | ### Features 19 | 20 | * adding dependent prerelease fix behind flag ([cc663d8](https://github.com/qiwi/multi-semantic-release/commit/cc663d87adb1074b28deb94c7b833bcc76fcc604)) 21 | 22 | # [7.0.0](https://github.com/qiwi/multi-semantic-release/compare/v6.7.0...v7.0.0) (2023-06-24) 23 | 24 | 25 | ### Features 26 | 27 | * update semver from v19 to v21 ([6e4c378](https://github.com/qiwi/multi-semantic-release/commit/6e4c378cd97edd08a6da7de8e5894493bc6f9b34)) 28 | 29 | 30 | ### BREAKING CHANGES 31 | 32 | * https://github.com/semantic-release/semantic-release/compare/v19.0.5...v21.0.5 33 | 34 | # [6.7.0](https://github.com/qiwi/multi-semantic-release/compare/v6.6.1...v6.7.0) (2023-05-25) 35 | 36 | 37 | ### Features 38 | 39 | * introduce `log-level` config option ([3fb6584](https://github.com/qiwi/multi-semantic-release/commit/3fb6584eeb19d4401dbdb85ae41d5a48d978d88c)) 40 | 41 | ## [6.6.1](https://github.com/qiwi/multi-semantic-release/compare/v6.6.0...v6.6.1) (2023-05-25) 42 | 43 | 44 | ### Performance Improvements 45 | 46 | * up deps ([f9e0c8a](https://github.com/qiwi/multi-semantic-release/commit/f9e0c8ac19aa986d6f4ce808b0520c33699564e0)) 47 | 48 | # [6.6.0](https://github.com/qiwi/multi-semantic-release/compare/v6.5.1...v6.6.0) (2023-05-23) 49 | 50 | 51 | ### Features 52 | 53 | * introduce a silent mode([#88](https://github.com/qiwi/multi-semantic-release/issues/88)) ([47a27d8](https://github.com/qiwi/multi-semantic-release/commit/47a27d82f4639281fc7fbbd53c074ac40014c17f)) 54 | 55 | ## [6.5.1](https://github.com/qiwi/multi-semantic-release/compare/v6.5.0...v6.5.1) (2022-06-14) 56 | 57 | 58 | ### Bug Fixes 59 | 60 | * ensure msr cli flags take precedence over their semrel equivalents ([2be75fa](https://github.com/qiwi/multi-semantic-release/commit/2be75fa0e38169f2c8233bbef0891e2e00f9fdb0)) 61 | 62 | # [6.5.0](https://github.com/qiwi/multi-semantic-release/compare/v6.4.0...v6.5.0) (2022-06-13) 63 | 64 | 65 | ### Features 66 | 67 | * added declarative config support ([c98ff10](https://github.com/qiwi/multi-semantic-release/commit/c98ff10a5f6993d9bdc46a51a5077525b93e127e)) 68 | 69 | # [6.4.0](https://github.com/qiwi/multi-semantic-release/compare/v6.3.0...v6.4.0) (2022-05-24) 70 | 71 | 72 | ### Features 73 | 74 | * support `${version}+${name}` tag format ([c53fefb](https://github.com/qiwi/multi-semantic-release/commit/c53fefb6e7ae8b3d16dbc235320c4f99d6ed4afc)), closes [#71](https://github.com/qiwi/multi-semantic-release/issues/71) 75 | * support `release.config.cjs` ([a6b9370](https://github.com/qiwi/multi-semantic-release/commit/a6b93706964fc5c60986392c21404ccdef4ad01b)) 76 | 77 | # [6.3.0](https://github.com/qiwi/multi-semantic-release/compare/v6.2.0...v6.3.0) (2022-05-11) 78 | 79 | 80 | ### Features 81 | 82 | * introduce `deps.prefix` flag to inject carets ([c3f4529](https://github.com/qiwi/multi-semantic-release/commit/c3f452976ac6642f8a285f2e9faa6521a5bba919)) 83 | 84 | # [6.2.0](https://github.com/qiwi/multi-semantic-release/compare/v6.1.1...v6.2.0) (2022-05-08) 85 | 86 | 87 | ### Features 88 | 89 | * prefer nested CLI flags ([1bf08cc](https://github.com/qiwi/multi-semantic-release/commit/1bf08ccbc722858febcaaaac2f8e0ba72638460c)) 90 | 91 | ## [6.1.1](https://github.com/qiwi/multi-semantic-release/compare/v6.1.0...v6.1.1) (2022-04-09) 92 | 93 | 94 | ### Bug Fixes 95 | 96 | * dont fetch tags if tags arg is empty array ([3a79558](https://github.com/qiwi/multi-semantic-release/commit/3a7955861d7782dd2257661d955a2adbf319627f)) 97 | * print queued pkg count instead of total ([d893a7f](https://github.com/qiwi/multi-semantic-release/commit/d893a7ff9f6f4d85377a1b61144842f3d07657a7)) 98 | 99 | # [6.1.0](https://github.com/qiwi/multi-semantic-release/compare/v6.0.2...v6.1.0) (2022-03-31) 100 | 101 | 102 | ### Features 103 | 104 | * introduce `ignorePrivate` flag ([e4891c9](https://github.com/qiwi/multi-semantic-release/commit/e4891c9f754afcc7fe87e84c390560a3fc5feef8)), closes [#66](https://github.com/qiwi/multi-semantic-release/issues/66) 105 | 106 | ## [6.0.2](https://github.com/qiwi/multi-semantic-release/compare/v6.0.1...v6.0.2) (2022-03-07) 107 | 108 | 109 | ### Bug Fixes 110 | 111 | * rm npm from peer deps ([307e505](https://github.com/qiwi/multi-semantic-release/commit/307e5057f12a9f616bdb432db2708e5193c34d4f)) 112 | 113 | ## [6.0.1](https://github.com/qiwi/multi-semantic-release/compare/v6.0.0...v6.0.1) (2022-03-06) 114 | 115 | 116 | ### Bug Fixes 117 | 118 | * fix export point ([cf31425](https://github.com/qiwi/multi-semantic-release/commit/cf314259697d8c7492c3c63da380b4207aae665a)) 119 | 120 | # [6.0.0](https://github.com/qiwi/multi-semantic-release/compare/v5.0.3...v6.0.0) (2022-03-06) 121 | 122 | 123 | ### Bug Fixes 124 | 125 | * up deps, fix some vuls ([2d5cf86](https://github.com/qiwi/multi-semantic-release/commit/2d5cf8660c0ebb2f97fa7476df1cd4fed63d07d1)) 126 | 127 | 128 | ### Code Refactoring 129 | 130 | * move to ESM ([99fffa9](https://github.com/qiwi/multi-semantic-release/commit/99fffa9c82b21a1578d376ac206e46d32e6b7ae3)) 131 | 132 | 133 | ### Features 134 | 135 | * replace synchronizer with @semrel-extra/topo ([ddd1032](https://github.com/qiwi/multi-semantic-release/commit/ddd10320487f5367aff15dd36c03c5bd60046b16)) 136 | 137 | 138 | ### BREAKING CHANGES 139 | 140 | * drop support for cycled monorepos 141 | * require Node.js v12 142 | 143 | ## [5.0.3](https://github.com/qiwi/multi-semantic-release/compare/v5.0.2...v5.0.3) (2022-03-05) 144 | 145 | 146 | ### Bug Fixes 147 | 148 | * fix nextType resolver ([606914b](https://github.com/qiwi/multi-semantic-release/commit/606914b43fc657340e243ca86430a7fff799210b)) 149 | 150 | ## [5.0.2](https://github.com/qiwi/multi-semantic-release/compare/v5.0.1...v5.0.2) (2022-03-05) 151 | 152 | 153 | ### Bug Fixes 154 | 155 | * add sync point for publish step ([adc823e](https://github.com/qiwi/multi-semantic-release/commit/adc823e1c4d2a952c58c8cf2f4fc00db7325ec53)) 156 | 157 | ## [5.0.1](https://github.com/qiwi/multi-semantic-release/compare/v5.0.0...v5.0.1) (2022-03-05) 158 | 159 | 160 | ### Bug Fixes 161 | 162 | * fix cascade bumping when some dep belongs to several levels of the dep tree ([cf20dea](https://github.com/qiwi/multi-semantic-release/commit/cf20deac25982d98fe795c507253ef389c0ecb81)) 163 | 164 | # [5.0.0](https://github.com/qiwi/multi-semantic-release/compare/v4.0.3...v5.0.0) (2022-03-04) 165 | 166 | 167 | ### Features 168 | 169 | * enable `sequentialPrepare` flag by default ([ad7e81f](https://github.com/qiwi/multi-semantic-release/commit/ad7e81f2e30b7673444dc31f39381b20afdf54bc)) 170 | 171 | 172 | ### BREAKING CHANGES 173 | 174 | * sequentialPrepare is set to true, to disable pass `--no-sequential-prepare` option 175 | 176 | ## [4.0.3](https://github.com/qiwi/multi-semantic-release/compare/v4.0.2...v4.0.3) (2022-03-01) 177 | 178 | 179 | ### Bug Fixes 180 | 181 | * update manifest version before npm plugin prepare step ([a1ae4c3](https://github.com/qiwi/multi-semantic-release/commit/a1ae4c3785498a3b22afcac278e431aaf99a1232)), closes [#58](https://github.com/qiwi/multi-semantic-release/issues/58) 182 | 183 | ## [4.0.2](https://github.com/qiwi/multi-semantic-release/compare/v4.0.1...v4.0.2) (2022-02-22) 184 | 185 | 186 | ### Bug Fixes 187 | 188 | * pin npm as peer dep ([67d8b80](https://github.com/qiwi/multi-semantic-release/commit/67d8b8095018b5d979795703f36d874a174baf07)) 189 | 190 | ## [4.0.1](https://github.com/qiwi/multi-semantic-release/compare/v4.0.0...v4.0.1) (2022-02-22) 191 | 192 | 193 | ### Bug Fixes 194 | 195 | * pin npm version to v8.4.1 ([22c89d1](https://github.com/qiwi/multi-semantic-release/commit/22c89d164ca6e4fb8fe5e9b7e3164685d543d5bd)), closes [#60](https://github.com/qiwi/multi-semantic-release/issues/60) 196 | 197 | # [4.0.0](https://github.com/qiwi/multi-semantic-release/compare/v3.17.1...v4.0.0) (2022-02-22) 198 | 199 | 200 | ### Features 201 | 202 | * bump semrel to v19 ([e6b8acb](https://github.com/qiwi/multi-semantic-release/commit/e6b8acb937e43ef28f311922e84434c75c8ad844)) 203 | 204 | 205 | ### BREAKING CHANGES 206 | 207 | * semrel npm plugin calls local npm version https://github.com/semantic-release/npm/pull/445 208 | 209 | ## [3.17.1](https://github.com/qiwi/multi-semantic-release/compare/v3.17.0...v3.17.1) (2021-12-29) 210 | 211 | 212 | ### Bug Fixes 213 | 214 | * fix globbing on Windows ([#57](https://github.com/qiwi/multi-semantic-release/issues/57)) ([1d71d21](https://github.com/qiwi/multi-semantic-release/commit/1d71d21bc0f95a921e390d59b80d0e2f4a3bd611)) 215 | 216 | # [3.17.0](https://github.com/qiwi/multi-semantic-release/compare/v3.16.0...v3.17.0) (2021-11-17) 217 | 218 | 219 | ### Features 220 | 221 | * handle .cjs configs ([38f657a](https://github.com/qiwi/multi-semantic-release/commit/38f657aea29097f55e5eafe0bd3f82790eded7f1)) 222 | 223 | # [3.16.0](https://github.com/qiwi/multi-semantic-release/compare/v3.15.0...v3.16.0) (2021-09-10) 224 | 225 | 226 | ### Features 227 | 228 | * add --tag-version-format flag ([259864c](https://github.com/qiwi/multi-semantic-release/commit/259864c2860b651435b4a8e1f01c3cfab350c590)) 229 | 230 | # [3.15.0](https://github.com/qiwi/multi-semantic-release/compare/v3.14.2...v3.15.0) (2021-07-08) 231 | 232 | 233 | ### Bug Fixes 234 | 235 | * update deps, fix some vuls ([f3cafc8](https://github.com/qiwi/multi-semantic-release/commit/f3cafc812ad58fb03c87065b477860ff5e58023c)) 236 | 237 | 238 | ### Features 239 | 240 | * add pnpm and bolt support ([91465de](https://github.com/qiwi/multi-semantic-release/commit/91465de637524b109aa4f6b898ff0e451ef39c36)) 241 | 242 | ## [3.14.2](https://github.com/qiwi/multi-semantic-release/compare/v3.14.1...v3.14.2) (2021-04-16) 243 | 244 | 245 | ### Bug Fixes 246 | 247 | * **sequential-prepare:** do not wait forever when a child package has no change ([713046a](https://github.com/qiwi/multi-semantic-release/commit/713046a251e9da435dad99ce7d5c7745aeff1871)) 248 | 249 | ## [3.14.1](https://github.com/qiwi/multi-semantic-release/compare/v3.14.0...v3.14.1) (2021-04-14) 250 | 251 | 252 | ### Bug Fixes 253 | 254 | * add flag to enable sequentialPrepare mode ([70c2dc5](https://github.com/qiwi/multi-semantic-release/commit/70c2dc54b393a9f0b9d7fca82a1f83a22bd2ff54)) 255 | 256 | # [3.14.0](https://github.com/qiwi/multi-semantic-release/compare/v3.13.2...v3.14.0) (2021-04-13) 257 | 258 | 259 | ### Features 260 | 261 | * allow to run prepare steps sequentially ([299748a](https://github.com/qiwi/multi-semantic-release/commit/299748adc7d6ca4f25db70557d90a124dd7d9f47)) 262 | * check that sequentialPrepare is not enabled on cyclic projects ([68c1198](https://github.com/qiwi/multi-semantic-release/commit/68c1198c395fb00c05fa97db66399f940575c9ee)) 263 | 264 | ## [3.13.2](https://github.com/qiwi/multi-semantic-release/compare/v3.13.1...v3.13.2) (2021-02-09) 265 | 266 | 267 | ### Bug Fixes 268 | 269 | * filter only tags that are valid ([59b61ad](https://github.com/qiwi/multi-semantic-release/commit/59b61ad1a5823c24d7ff4237edf821fffb04abff)) 270 | * getVersionFromTag tests to fix release process ([e7f1646](https://github.com/qiwi/multi-semantic-release/commit/e7f16466bccc39311da8017dd5f0f7a8ae57686d)) 271 | 272 | ## [3.13.1](https://github.com/qiwi/multi-semantic-release/compare/v3.13.0...v3.13.1) (2021-02-05) 273 | 274 | 275 | ### Bug Fixes 276 | 277 | * **package:** up deps, fix some vuls ([d8905b0](https://github.com/qiwi/multi-semantic-release/commit/d8905b09152972a30d906ac18fca686b65e61591)) 278 | 279 | # [3.13.0](https://github.com/qiwi/multi-semantic-release/compare/v3.12.6...v3.13.0) (2021-02-04) 280 | 281 | 282 | ### Features 283 | 284 | * ignore packages to be released with workspaces and CLI ([#42](https://github.com/qiwi/multi-semantic-release/issues/42)) ([b98e181](https://github.com/qiwi/multi-semantic-release/commit/b98e181978294bbca7ae7f1a9ab7a9ecf0819847)) 285 | 286 | ## [3.12.6](https://github.com/qiwi/multi-semantic-release/compare/v3.12.5...v3.12.6) (2021-02-01) 287 | 288 | 289 | ### Bug Fixes 290 | 291 | * wrong context.commits when have multiple releases commit ([f82f125](https://github.com/qiwi/multi-semantic-release/commit/f82f1258255b7016c45405cc91e3056a1701e92d)) 292 | 293 | ## [3.12.5](https://github.com/qiwi/multi-semantic-release/compare/v3.12.4...v3.12.5) (2021-01-22) 294 | 295 | 296 | ### Bug Fixes 297 | 298 | * **update-deps:** properly resolve next pre-versions ([62b348e](https://github.com/qiwi/multi-semantic-release/commit/62b348eb87ccbccf21380ea0153036ef9792dcaa)) 299 | 300 | ## [3.12.4](https://github.com/qiwi/multi-semantic-release/compare/v3.12.3...v3.12.4) (2021-01-22) 301 | 302 | 303 | ### Bug Fixes 304 | 305 | * bump-up of pre-version ([02ef270](https://github.com/qiwi/multi-semantic-release/commit/02ef270865c45e06bf8d2761359fd53b08cadb70)) 306 | 307 | ## [3.12.3](https://github.com/qiwi/multi-semantic-release/compare/v3.12.2...v3.12.3) (2021-01-05) 308 | 309 | 310 | ### Performance Improvements 311 | 312 | * refactor nextPreVersion to reduce cognitive complexity ([#35](https://github.com/qiwi/multi-semantic-release/issues/35)) ([601bbd3](https://github.com/qiwi/multi-semantic-release/commit/601bbd35d969167c5b54cb0e8bad2be0a2165705)) 313 | 314 | ## [3.12.2](https://github.com/qiwi/multi-semantic-release/compare/v3.12.1...v3.12.2) (2021-01-05) 315 | 316 | 317 | ### Bug Fixes 318 | 319 | * local dependencies correct bump from release to prerelease ([#34](https://github.com/qiwi/multi-semantic-release/issues/34)) ([6481a59](https://github.com/qiwi/multi-semantic-release/commit/6481a590e52224405793ea375fbd689120341a42)) 320 | 321 | ## [3.12.1](https://github.com/qiwi/multi-semantic-release/compare/v3.12.0...v3.12.1) (2020-12-30) 322 | 323 | 324 | ### Bug Fixes 325 | 326 | * avoid non-updated local dependencies bump ([#33](https://github.com/qiwi/multi-semantic-release/issues/33)) ([9faeef6](https://github.com/qiwi/multi-semantic-release/commit/9faeef65538535a5611730e016a3cbd3f454052b)) 327 | 328 | # [3.12.0](https://github.com/qiwi/multi-semantic-release/compare/v3.11.2...v3.12.0) (2020-12-30) 329 | 330 | 331 | ### Features 332 | 333 | * **debug:** attach pkg prefixes to debug notes ([25e111f](https://github.com/qiwi/multi-semantic-release/commit/25e111f4b85e20c06d518abc1245918788f730a5)) 334 | 335 | ## [3.11.2](https://github.com/qiwi/multi-semantic-release/compare/v3.11.1...v3.11.2) (2020-12-29) 336 | 337 | 338 | ### Bug Fixes 339 | 340 | * **config:** fix `options` and `plugins` resolver ([56e974c](https://github.com/qiwi/multi-semantic-release/commit/56e974cec12f47d50f967d681506a84418b4e6bb)) 341 | 342 | ## [3.11.1](https://github.com/qiwi/multi-semantic-release/compare/v3.11.0...v3.11.1) (2020-12-28) 343 | 344 | 345 | ### Bug Fixes 346 | 347 | * fix pkgOptions resolver ([2a2f0cf](https://github.com/qiwi/multi-semantic-release/commit/2a2f0cf43888f2d6acfc174f12993948947f0111)) 348 | 349 | # [3.11.0](https://github.com/qiwi/multi-semantic-release/compare/v3.10.0...v3.11.0) (2020-12-24) 350 | 351 | 352 | ### Features 353 | 354 | * **debug:** log manifest deps changes ([88b4077](https://github.com/qiwi/multi-semantic-release/commit/88b40770d2cdaaf30912745f4fc4eee44357d9ea)), closes [#27](https://github.com/qiwi/multi-semantic-release/issues/27) 355 | 356 | # [3.10.0](https://github.com/qiwi/multi-semantic-release/compare/v3.9.5...v3.10.0) (2020-12-23) 357 | 358 | 359 | ### Features 360 | 361 | * provide pre-release flow ([6a9ce16](https://github.com/qiwi/multi-semantic-release/commit/6a9ce165c44aba3b8ef787cdfa575ad8b0dc3f05)), closes [#25](https://github.com/qiwi/multi-semantic-release/issues/25) 362 | 363 | ## [3.9.5](https://github.com/qiwi/multi-semantic-release/compare/v3.9.4...v3.9.5) (2020-11-23) 364 | 365 | 366 | ### Bug Fixes 367 | 368 | * fix getNextVersion resolver ([7275ae7](https://github.com/qiwi/multi-semantic-release/commit/7275ae75799ad3cb3bca34c9cfa2978fed092794)) 369 | 370 | ## [3.9.4](https://github.com/qiwi/multi-semantic-release/compare/v3.9.3...v3.9.4) (2020-11-23) 371 | 372 | 373 | ### Bug Fixes 374 | 375 | * apply deps update before npm's plugin prepare ([77b6ee2](https://github.com/qiwi/multi-semantic-release/commit/77b6ee2e8d1a281d359520fe12e548ea79776b5e)) 376 | 377 | ## [3.9.3](https://github.com/qiwi/multi-semantic-release/compare/v3.9.2...v3.9.3) (2020-11-23) 378 | 379 | 380 | ### Bug Fixes 381 | 382 | * trigger next pkg `prepare` after the prev `publish` ([f74d185](https://github.com/qiwi/multi-semantic-release/commit/f74d185c268567585393b977817ab69f3225532a)) 383 | 384 | ## [3.9.2](https://github.com/qiwi/multi-semantic-release/compare/v3.9.1...v3.9.2) (2020-11-23) 385 | 386 | 387 | ### Performance Improvements 388 | 389 | * up deps, minor code improvements ([a7aa625](https://github.com/qiwi/multi-semantic-release/commit/a7aa625f6325c5fd8cb1a0ca8e1029b5ea082950)) 390 | 391 | ## [3.9.1](https://github.com/qiwi/multi-semantic-release/compare/v3.9.0...v3.9.1) (2020-11-21) 392 | 393 | 394 | ### Performance Improvements 395 | 396 | * **package:** up deps ([506a0e8](https://github.com/qiwi/multi-semantic-release/commit/506a0e80d9152427eb8dca5c9032754fce4e8a22)) 397 | 398 | # [3.9.0](https://github.com/qiwi/multi-semantic-release/compare/v3.8.5...v3.9.0) (2020-11-21) 399 | 400 | 401 | ### Bug Fixes 402 | 403 | * sync pkg version after running the npm plugin ([1d24e45](https://github.com/qiwi/multi-semantic-release/commit/1d24e45c7ebba1b6b97ef259e98ea17fbb5d9d35)) 404 | 405 | 406 | ### Features 407 | 408 | * add dependencies updating controller ([0c9b040](https://github.com/qiwi/multi-semantic-release/commit/0c9b0406c35cedb264ac321805da4e93b9f2d5d8)) 409 | 410 | ## [3.8.5](https://github.com/qiwi/multi-semantic-release/compare/v3.8.4...v3.8.5) (2020-10-02) 411 | 412 | 413 | ### Bug Fixes 414 | 415 | * specify used but forgotten dependencies ([73def7f](https://github.com/qiwi/multi-semantic-release/commit/73def7f4dcfccf12c9cf3a0d998889ea1cd53b10)) 416 | 417 | ## [3.8.4](https://github.com/qiwi/multi-semantic-release/compare/v3.8.3...v3.8.4) (2020-09-23) 418 | 419 | 420 | ### Bug Fixes 421 | 422 | * **package:** up deps, fix vulns ([5a4d91e](https://github.com/qiwi/multi-semantic-release/commit/5a4d91e879397acc0d8b08af6337817f66491739)) 423 | 424 | ## [3.8.3](https://github.com/qiwi/multi-semantic-release/compare/v3.8.2...v3.8.3) (2020-08-04) 425 | 426 | 427 | ### Bug Fixes 428 | 429 | * preserve trailing whitespace in manifest ([06426ec](https://github.com/qiwi/multi-semantic-release/commit/06426ec05f04cdcdf1561a4d1220890cf8ab1421)) 430 | 431 | ## [3.8.2](https://github.com/qiwi/multi-semantic-release/compare/v3.8.1...v3.8.2) (2020-08-03) 432 | 433 | 434 | ### Bug Fixes 435 | 436 | * process optional deps during manifest update ([4b7066c](https://github.com/qiwi/multi-semantic-release/commit/4b7066c9b78169638baed7f7faefbbcd3705f5a4)) 437 | 438 | ## [3.8.1](https://github.com/qiwi/multi-semantic-release/compare/v3.8.0...v3.8.1) (2020-07-31) 439 | 440 | 441 | ### Bug Fixes 442 | 443 | * fix internal flag ref filterParent → firstParent ([8c7400f](https://github.com/qiwi/multi-semantic-release/commit/8c7400fe2f639f5d7abfc9219efb1b331c585fc8)) 444 | 445 | # [3.8.0](https://github.com/qiwi/multi-semantic-release/compare/v3.7.0...v3.8.0) (2020-07-24) 446 | 447 | 448 | ### Features 449 | 450 | * enable first-parent commits filtering by cli flag ([33306cc](https://github.com/qiwi/multi-semantic-release/commit/33306cce3e6e7d118a74690ecf3b33ac4154eac9)) 451 | 452 | # [3.7.0](https://github.com/qiwi/multi-semantic-release/compare/v3.6.0...v3.7.0) (2020-07-24) 453 | 454 | 455 | ### Features 456 | 457 | * log filtered commits in debug ([c64b8e1](https://github.com/qiwi/multi-semantic-release/commit/c64b8e1a4e97b7164b80f267d631dbccb459a64d)) 458 | 459 | # [3.6.0](https://github.com/qiwi/multi-semantic-release/compare/v3.5.1...v3.6.0) (2020-07-21) 460 | 461 | 462 | ### Features 463 | 464 | * add some debug messages ([ec792e1](https://github.com/qiwi/multi-semantic-release/commit/ec792e15a4223880054186035571f8ca647add0e)) 465 | 466 | ## [3.5.1](https://github.com/qiwi/multi-semantic-release/compare/v3.5.0...v3.5.1) (2020-07-20) 467 | 468 | 469 | ### Performance Improvements 470 | 471 | * various synchronizer optimizations ([87a7602](https://github.com/qiwi/multi-semantic-release/commit/87a760293fd97ab3aeaf361a256659897c96af9c)) 472 | 473 | # [3.5.0](https://github.com/qiwi/multi-semantic-release/compare/v3.4.0...v3.5.0) (2020-07-20) 474 | 475 | 476 | ### Features 477 | 478 | * **debug:** print passed cli flags ([d720cd7](https://github.com/qiwi/multi-semantic-release/commit/d720cd717b2b99fa32a2d6a438a5142ebc453c26)) 479 | 480 | # [3.4.0](https://github.com/qiwi/multi-semantic-release/compare/v3.3.0...v3.4.0) (2020-07-19) 481 | 482 | 483 | ### Features 484 | 485 | * add sequential-init flag to avoid hypothetical concurrent initialization collisions ([348678e](https://github.com/qiwi/multi-semantic-release/commit/348678ef1997ec914c0471bd60e47b987244ba54)) 486 | 487 | # [3.3.0](https://github.com/qiwi/multi-semantic-release/compare/v3.2.0...v3.3.0) (2020-07-17) 488 | 489 | 490 | ### Features 491 | 492 | * support workspace.packages notation ([4a606b2](https://github.com/qiwi/multi-semantic-release/commit/4a606b2ceffcd70e18c015d51e62cda78f4cf58e)) 493 | 494 | # [3.2.0](https://github.com/qiwi/multi-semantic-release/compare/v3.1.0...v3.2.0) (2020-07-14) 495 | 496 | 497 | ### Features 498 | 499 | * apply --first-parent filter to commits ([14a896b](https://github.com/qiwi/multi-semantic-release/commit/14a896b5e501e6c900b8d89ee16ff17289c1d3a1)) 500 | 501 | # [3.1.0](https://github.com/qiwi/multi-semantic-release/compare/v3.0.3...v3.1.0) (2020-07-07) 502 | 503 | 504 | ### Features 505 | 506 | * uphold the prev package.json indents ([ac5832f](https://github.com/qiwi/multi-semantic-release/commit/ac5832f20c8218c95fc46719cbd532732db53419)) 507 | 508 | ## [3.0.3](https://github.com/qiwi/multi-semantic-release/compare/v3.0.2...v3.0.3) (2020-06-26) 509 | 510 | 511 | ### Bug Fixes 512 | 513 | * override env.TRAVIS_PULL_REQUEST_BRANCH to fix PR checks on travis-ci ([e4b1929](https://github.com/qiwi/multi-semantic-release/commit/e4b192971f3cba0ad8e341cdc64116285a9de220)), closes [#11](https://github.com/qiwi/multi-semantic-release/issues/11) 514 | 515 | ## [3.0.2](https://github.com/qiwi/multi-semantic-release/compare/v3.0.1...v3.0.2) (2020-06-16) 516 | 517 | 518 | ### Bug Fixes 519 | 520 | * allow any `todo` package to run the `generateNotes` queue ([26a87d7](https://github.com/qiwi/multi-semantic-release/commit/26a87d7f88c09417b47b55fe6e21edb6d13f5520)), closes [#9](https://github.com/qiwi/multi-semantic-release/issues/9) 521 | 522 | ## [3.0.1](https://github.com/qiwi/multi-semantic-release/compare/v3.0.0...v3.0.1) (2020-06-05) 523 | 524 | 525 | ### Bug Fixes 526 | 527 | * filter queued packages on generateNotes stage ([e0625ce](https://github.com/qiwi/multi-semantic-release/commit/e0625ced9c52d027246b58305d8206c949b4958c)), closes [#6](https://github.com/qiwi/multi-semantic-release/issues/6) 528 | 529 | # [3.0.0](https://github.com/qiwi/multi-semantic-release/compare/v2.6.4...v3.0.0) (2020-06-04) 530 | 531 | 532 | ### Features 533 | 534 | * **engine:** up nodejs version ([10af385](https://github.com/qiwi/multi-semantic-release/commit/10af3850de9cc04a1f4a85c505752b659e3f47ff)) 535 | 536 | 537 | ### BREAKING CHANGES 538 | 539 | * **engine:** the latest semantic-release requires Node.js 10.18 540 | 541 | ## [2.6.4](https://github.com/qiwi/multi-semantic-release/compare/v2.6.3...v2.6.4) (2020-05-28) 542 | 543 | 544 | ### Performance Improvements 545 | 546 | * deps revision ([4f62817](https://github.com/qiwi/multi-semantic-release/commit/4f628175669ef025f91b2c2cdacafd37252a7f87)) 547 | 548 | ## [2.6.3](https://github.com/qiwi/multi-semantic-release/compare/v2.6.2...v2.6.3) (2020-05-28) 549 | 550 | 551 | ### Performance Improvements 552 | 553 | * straighten plugins execution pipeline ([e57fe2f](https://github.com/qiwi/multi-semantic-release/commit/e57fe2fc3274d66520fbeacdac488b1a29430835)), closes [#4](https://github.com/qiwi/multi-semantic-release/issues/4) 554 | 555 | ## [2.6.2](https://github.com/qiwi/multi-semantic-release/compare/v2.6.1...v2.6.2) (2020-05-22) 556 | 557 | 558 | ### Bug Fixes 559 | 560 | * beautify log labels ([78cbc8a](https://github.com/qiwi/multi-semantic-release/commit/78cbc8a773076c370f43894d35423a0a23b0cafb)) 561 | 562 | ## [2.6.1](https://github.com/qiwi/multi-semantic-release/compare/v2.6.0...v2.6.1) (2020-05-22) 563 | 564 | 565 | ### Bug Fixes 566 | 567 | * provide partial release ([898998a](https://github.com/qiwi/multi-semantic-release/commit/898998a25f100e3450b14dd55ea6c468d3a02432)) 568 | 569 | # [2.6.0](https://github.com/qiwi/multi-semantic-release/compare/v2.5.0...v2.6.0) (2020-05-21) 570 | 571 | 572 | ### Features 573 | 574 | * let publish step run in parallel ([4d5c451](https://github.com/qiwi/multi-semantic-release/commit/4d5c451e9400437820002a54a297a3c031856997)) 575 | 576 | # [2.5.0](https://github.com/qiwi/multi-semantic-release/compare/v2.4.3...v2.5.0) (2020-05-20) 577 | 578 | 579 | ### Bug Fixes 580 | 581 | * publish updated deps ([791f55a](https://github.com/qiwi/multi-semantic-release/commit/791f55a34574f5e6531ed69f182ffd54b68e2450)), closes [#1](https://github.com/qiwi/multi-semantic-release/issues/1) 582 | 583 | 584 | ### Features 585 | 586 | * add execa queued hook ([042933e](https://github.com/qiwi/multi-semantic-release/commit/042933eb619e3fb7111a8e1d4a41e3593e2729ca)) 587 | * apply queuefy to plugin methods instead of execa ([9ae7d0d](https://github.com/qiwi/multi-semantic-release/commit/9ae7d0d8c1cc9300517733fe9e4edcb557069dff)) 588 | 589 | ## [2.4.3](https://github.com/qiwi/multi-semantic-release/compare/v2.4.2...v2.4.3) (2020-03-08) 590 | 591 | 592 | ### Performance Improvements 593 | 594 | * log yarn paths ([3896d5c](https://github.com/qiwi/multi-semantic-release/commit/3896d5ca4e97870e37d2f254e3586cb5b9165b84)) 595 | 596 | ## [2.4.2](https://github.com/qiwi/multi-semantic-release/compare/v2.4.1...v2.4.2) (2020-03-07) 597 | 598 | 599 | ### Bug Fixes 600 | 601 | * make logger to be singleton ([1790794](https://github.com/qiwi/multi-semantic-release/commit/179079420368d4a33505adca3079c06eddad928d)) 602 | 603 | ## [2.4.1](https://github.com/qiwi/multi-semantic-release/compare/v2.4.0...v2.4.1) (2020-03-07) 604 | 605 | 606 | ### Performance Improvements 607 | 608 | * log improvements ([c45dccc](https://github.com/qiwi/multi-semantic-release/commit/c45dcccd3e81b9ce0489a920ce16f9616b4ebda0)) 609 | 610 | # [2.4.0](https://github.com/qiwi/multi-semantic-release/compare/v2.3.3...v2.4.0) (2020-03-07) 611 | 612 | 613 | ### Features 614 | 615 | * log manifest path ([db451e8](https://github.com/qiwi/multi-semantic-release/commit/db451e877e53e54487019978ee20bd03c64fa992)) 616 | 617 | ## [2.3.3](https://github.com/qiwi/multi-semantic-release/compare/v2.3.2...v2.3.3) (2020-03-07) 618 | 619 | 620 | ### Bug Fixes 621 | 622 | * fix logger path ([232d2dc](https://github.com/qiwi/multi-semantic-release/commit/232d2dc62ce440bf58ff7d05df5d27f72f24cfe6)) 623 | 624 | ## [2.3.2](https://github.com/qiwi/multi-semantic-release/compare/v2.3.1...v2.3.2) (2020-03-07) 625 | 626 | 627 | ### Performance Improvements 628 | 629 | * log multi-sem-rel flags ([75389e0](https://github.com/qiwi/multi-semantic-release/commit/75389e06b97f39bfd3e5a797677ee835bc569557)) 630 | 631 | ## [2.3.1](https://github.com/qiwi/multi-semantic-release/compare/v2.3.0...v2.3.1) (2020-03-07) 632 | 633 | 634 | ### Bug Fixes 635 | 636 | * fix debug logging ([71527b2](https://github.com/qiwi/multi-semantic-release/commit/71527b2578a0a559ac1b175936d72e043258fe04)) 637 | 638 | # [2.3.0](https://github.com/qiwi/multi-semantic-release/compare/v2.2.0...v2.3.0) (2020-03-06) 639 | 640 | 641 | ### Features 642 | 643 | * add debugger ([d2c090d](https://github.com/qiwi/multi-semantic-release/commit/d2c090daa104d19d71a09e54c7ff396d0f5824df)) 644 | 645 | # [2.2.0](https://github.com/qiwi/multi-semantic-release/compare/v2.1.3...v2.2.0) (2020-03-06) 646 | 647 | 648 | ### Features 649 | 650 | * add meow as cli provider ([6de93b9](https://github.com/qiwi/multi-semantic-release/commit/6de93b9d7d5f818cccf5a376222e091d3be34065)) 651 | 652 | ## [2.1.3](https://github.com/qiwi/multi-semantic-release/compare/v2.1.2...v2.1.3) (2020-03-05) 653 | 654 | 655 | ### Bug Fixes 656 | 657 | * try to prevent deps update rollback ([9108350](https://github.com/qiwi/multi-semantic-release/commit/910835046bc6dc5e63d637d83276d945df6f1943)) 658 | 659 | ## [2.1.2](https://github.com/qiwi/multi-semantic-release/compare/v2.1.1...v2.1.2) (2020-02-13) 660 | 661 | 662 | ### Bug Fixes 663 | 664 | * **cli:** fix inner spawnhook call ([70aa292](https://github.com/qiwi/multi-semantic-release/commit/70aa2927cd52a374f1626ab514a836bc9d98edaa)) 665 | 666 | ## [2.1.1](https://github.com/qiwi/multi-semantic-release/compare/v2.1.0...v2.1.1) (2020-02-13) 667 | 668 | 669 | ### Bug Fixes 670 | 671 | * **cli:** restore watchspawn context ([56145aa](https://github.com/qiwi/multi-semantic-release/commit/56145aaae5b43065c9925ed661302216e649112f)) 672 | 673 | # [2.1.0](https://github.com/qiwi/multi-semantic-release/compare/v2.0.1...v2.1.0) (2020-02-13) 674 | 675 | 676 | ### Features 677 | 678 | * add process.spawn arg watcher ([7699b6f](https://github.com/qiwi/multi-semantic-release/commit/7699b6f4058934cbcf51b196d7f6aaa13372b35a)) 679 | 680 | ## [2.0.1](https://github.com/qiwi/multi-semantic-release/compare/v2.0.0...v2.0.1) (2020-02-11) 681 | 682 | 683 | ### Performance Improvements 684 | 685 | * **package:** up deps ([6b903a7](https://github.com/qiwi/multi-semantic-release/commit/6b903a7e318326c53cbd9cfb201546d84e650c32)) 686 | 687 | # [2.0.0](https://github.com/qiwi/multi-semantic-release/compare/v1.2.0...v2.0.0) (2020-01-19) 688 | 689 | 690 | ### Features 691 | 692 | * drop nodejs v8 support ([80f0a24](https://github.com/qiwi/multi-semantic-release/commit/80f0a242e195c6b4f7d6a68cdcbff9a25cd5577f)) 693 | 694 | 695 | ### Performance Improvements 696 | 697 | * **package:** up deps & tech release ([bf00b41](https://github.com/qiwi/multi-semantic-release/commit/bf00b41662805ba87d67865830c7b0d8d88e20b9)) 698 | 699 | 700 | ### BREAKING CHANGES 701 | 702 | * drop nodejs v8 703 | 704 | # [1.2.0](https://github.com/qiwi/multi-semantic-release/compare/v1.1.0...v1.2.0) (2019-11-03) 705 | 706 | 707 | ### Features 708 | 709 | * tech release ([828a82d](https://github.com/qiwi/multi-semantic-release/commit/828a82d88d0692aa3bc19dd8dae5674a46a719ce)) 710 | 711 | # [1.1.0](https://github.com/qiwi/multi-semantic-release/compare/v1.0.3...v1.1.0) (2019-11-03) 712 | 713 | 714 | ### Bug Fixes 715 | 716 | * **package:** add missed sem-rel plugins ([f3c9318](https://github.com/qiwi/multi-semantic-release/commit/f3c9318aa76ac7ec10c4bb45782d8d775b1885d9)) 717 | * **package:** update execa to be compatible with sem-rel 15.13.28 ([069bb4e](https://github.com/qiwi/multi-semantic-release/commit/069bb4e943223a712398b35f78abcfc2a43b6323)), closes [#7](https://github.com/qiwi/multi-semantic-release/issues/7) 718 | * force a release ([1e3ece5](https://github.com/qiwi/multi-semantic-release/commit/1e3ece5e03a5e39a2e100edaf8bead26245d0a12)) 719 | 720 | 721 | ### Features 722 | 723 | * add execasync CLI flag to make execa calls be always synchronous ([693438c](https://github.com/qiwi/multi-semantic-release/commit/693438caeb64af083a6db4e6634aaac74da112ad)), closes [#1](https://github.com/qiwi/multi-semantic-release/issues/1) 724 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (C) 2017 by Dave Houlbrooke 2 | Permission to use, copy, modify, and/or distribute this software 3 | for any purpose with or without fee is hereby granted. 4 | 5 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL 6 | WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED 7 | WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL 8 | THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR 9 | CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 10 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, 11 | NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN 12 | CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @qiwi/multi-semantic-release 2 | hacky [semantic-release](https://github.com/semantic-release/semantic-release) for monorepos 3 | 4 | [![Travis CI](https://travis-ci.com/qiwi/multi-semantic-release.svg?branch=master)](https://travis-ci.com/qiwi/multi-semantic-release) 5 | [![Maintainability](https://api.codeclimate.com/v1/badges/c6ee027803a794f1d67d/maintainability)](https://codeclimate.com/github/qiwi/multi-semantic-release/maintainability) 6 | [![Test Coverage](https://api.codeclimate.com/v1/badges/c6ee027803a794f1d67d/test_coverage)](https://codeclimate.com/github/qiwi/multi-semantic-release/test_coverage) 7 | [![npm (scoped)](https://img.shields.io/npm/v/@qiwi/multi-semantic-release)](https://www.npmjs.com/package/@qiwi/multi-semantic-release) 8 | 9 | This fork of [dhoub/multi-semantic-release](https://github.com/dhoulb/multi-semantic-release) replaces [`setImmediate` loops](https://github.com/dhoulb/multi-semantic-release/blob/561a8e66133d422d88008c32c479d1148876aba4/lib/wait.js#L13) 10 | and [`execa.sync` hooks](https://github.com/dhoulb/multi-semantic-release/blob/561a8e66133d422d88008c32c479d1148876aba4/lib/execaHook.js#L5) with event-driven flow and finally makes possible to run the most release operations in parallel. 11 | 🎉 🎉 🎉 12 | 13 | 14 | ## Status 15 | We're still using this lib as a part of our release infra, but we're gradually migrating to [bulk-release](https://github.com/semrel-extra/zx-bulk-release). 16 | This means that we do not have many resources to develop this implementation actively, but we will continue to do it on a leftover basis. 17 | 18 | ## Install 19 | 20 | ```sh 21 | yarn add @qiwi/multi-semantic-release --dev 22 | ``` 23 | 24 | ## Usage 25 | 26 | ```sh 27 | multi-semantic-release 28 | ``` 29 | 30 | ## Configuring Multi-Semantic-Release 31 | 32 | multi-semantic-release can be configured a number of ways: 33 | 34 | * A `.multi-releaserc` file, written in YAML or JSON, with optional extensions: `.yaml`/ `.yml`/ `.json`/ `.js` 35 | * A `multi-release.config.js` file that exports an object 36 | * A `multi-release` key in the workspace root package.json 37 | 38 | Alternatively some options may be set via CLI flags. 39 | 40 | **Note:** CLI arguments take precedence over options configured in the configuration file. 41 | 42 | ### Options 43 | 44 | | Option | Type | CLI Flag | Description | 45 | |-------------------|-------------------|------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 46 | | dryRun | `boolean` | `--dry-run` | Dry run mode. | 47 | | logLevel | `String` | `--log-level` | Sets the internal logger verbosity level: `error, warn, info, debug, trace`. Defaults to `info`. | 48 | | debug | `boolean` | `--debug` | Output debugging information. Shortcut for `--logLevel=debug`. | 49 | | silent | `boolean` | `--silent` | Turns off any log outputs. | 50 | | extends | `String \| Array` | N/A | List of modules or file paths containing a shareable configuration. If multiple shareable configurations are set, they will be imported in the order defined with each configuration option taking precedence over the options defined in the previous. | 51 | | sequentialInit | `boolean` | `--sequential-init` | Avoid hypothetical concurrent initialization collisions. | 52 | | sequentialPrepare | `boolean` | `--sequential-prepare` | Avoid hypothetical concurrent preparation collisions. **True by default.** | 53 | | firstParent | `boolean` | `--first-parent` | Apply commit filtering to current branch only. | 54 | | ignorePrivate | `boolean` | `--ignore-private` | Exclude private packages. **True by default.** | 55 | | ignorePackages | `String \| Array` | `--ignore-packages` | Packages list to be ignored on bumping process (appended to the ones that already exist at package.json workspaces). If using the CLI flag, supply a comma seperated list of strings. | 56 | | tagFormat | `String` | `--tag-format` | Format to use when creating tag names. Should include "name" and "version" vars. Default: `"${name}@${version}"` which generates "package-name@1.0.0" | 57 | | deps | `Object` | N/A | Dependency handling, see below for possible values. | 58 | 59 | ### `deps` Options 60 | 61 | | Option | Type | CLI Flag | Description | 62 | |-----------------------|--------------------------------------|--------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 63 | | bump | `override \| satisfy \| inherit` | `--deps.bump` | Define deps version updating rule. Allowed: override, satisfy, inherit. **`override` by default.** | 64 | | release | `patch \| minor \| major \| inherit` | `--deps.release` | Define release type for dependent package if any of its deps changes. Supported values: patch, minor, major, inherit. **`patch` by default** | 65 | | prefix | `'^' \| '~' \| ''` | `--deps.prefix` | Optional prefix to be attached to the next version if `bump` is set to `override`. **`''` by default**. | 66 | | pullTagsForPrerelease | `boolean` | `--deps.pullTagsForPrerelease` | Optional flag to use release tags for evaluating prerelease version bumping. Normally, this option will lead to dumping dependencies to a version past what was just released and tagged by semantic release. Only set this option to true if you previously had a workflow that compensated for the previous bug behavior. **`'false'` by default**. | 67 | 68 | 69 | ### Examples 70 | 71 | * Via multi-release key in the project's package.json file: 72 | 73 | ```json 74 | { 75 | "multi-release": { 76 | "ignorePackages": [ 77 | "!packages/b/**", 78 | "!packages/c/**" 79 | ], 80 | "deps": { 81 | "bump": "inherit" 82 | } 83 | } 84 | } 85 | ``` 86 | 87 | * Via `.multi-releaserc` file: 88 | 89 | ```json 90 | { 91 | "ignorePackages": [ 92 | "!packages/b/**", 93 | "!packages/c/**" 94 | ], 95 | "deps": { 96 | "bump": "inherit" 97 | } 98 | } 99 | ``` 100 | 101 | * Via CLI: 102 | 103 | ```sh 104 | $ multi-semantic-release --ignore-packages=packages/a/**,packages/b/** --deps.bump=inherit 105 | ``` 106 | 107 | ## Configuring Semantic-Release 108 | **MSR** requires **semrel** config to be added [in any supported format](https://github.com/semantic-release/semantic-release/blob/master/docs/usage/configuration.md#configuration) for each package or/and declared in repo root (`globalConfig` is extremely useful if all the modules have the same strategy of release). 109 | NOTE config resolver joins `globalConfig` and `packageConfig` during execution. 110 | ```javascript 111 | // Load the package-specific options. 112 | const { options: pkgOptions } = await getConfig(dir); 113 | 114 | // The 'final options' are the global options merged with package-specific options. 115 | // We merge this ourselves because package-specific options can override global options. 116 | const finalOptions = Object.assign({}, globalOptions, pkgOptions); 117 | ``` 118 | 119 | Make sure to have a `workspaces` attribute inside your `package.json` project file. In there, you can set a list of packages that you might want to process in the msr process, as well as ignore others. For example, let's say your project has 4 packages (i.e. a, b, c and d) and you want to process only a and d (ignore b and c). You can set the following structure in your `package.json` file: 120 | 121 | ```json 122 | { 123 | "name": "msr-test-yarn", 124 | "author": "Dave Houlbrooke =8.3" 130 | }, 131 | "workspaces": [ 132 | "packages/*", 133 | "!packages/b/**", 134 | "!packages/c/**" 135 | ], 136 | "release": { 137 | "plugins": [ 138 | "@semantic-release/commit-analyzer", 139 | "@semantic-release/release-notes-generator" 140 | ], 141 | "noCi": true 142 | } 143 | } 144 | ``` 145 | 146 | You can also ignore it with the CLI: 147 | 148 | ```bash 149 | $ multi-semantic-release --ignore-packages=packages/b/**,packages/c/** 150 | ``` 151 | 152 | You can also combine the CLI ignore options with the `!` operator at each package inside `workspaces` attribute. Even though you can use the CLI to ignore options, you can't use it to set which packages to be released – i.e. you still need to set the `workspaces` attribute inside the `package.json`. 153 | 154 | ## Verified usage examples 155 | We use this tool to release our JS platform code inhouse (GitHub Enterprise + JB TeamCity) and for our OSS (GitHub + Travis CI). Guaranteed working configurations available in projects. 156 | * [qiwi/substrate](https://github.com/qiwi/substrate) 157 | * [qiwi/json-rpc](https://github.com/qiwi/json-rpc) 158 | * [qiwi/lint-config-qiwi](https://github.com/qiwi/lint-config-qiwi) 159 | 160 | ## License 161 | [0BSD](./LICENSE.md) 162 | -------------------------------------------------------------------------------- /bin/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import meow from "meow"; 4 | import process from "process"; 5 | import { toPairs, set } from "lodash-es"; 6 | import { logger } from "../lib/logger.js"; 7 | 8 | const cli = meow( 9 | ` 10 | Usage 11 | $ multi-semantic-release 12 | 13 | Options 14 | --dry-run Dry run mode. 15 | --debug Output debugging information. 16 | --silent Do not print configuration information. 17 | --sequential-init Avoid hypothetical concurrent initialization collisions. 18 | --sequential-prepare Avoid hypothetical concurrent preparation collisions. Do not use if your project have cyclic dependencies. 19 | --first-parent Apply commit filtering to current branch only. 20 | --deps.bump Define deps version updating rule. Allowed: override, satisfy, inherit. 21 | --deps.release Define release type for dependent package if any of its deps changes. Supported values: patch, minor, major, inherit. 22 | --deps.prefix Optional prefix to be attached to the next dep version if '--deps.bump' set to 'override'. Supported values: '^' | '~' | '' (empty string as default). 23 | --deps.pullTagsForPrerelease Optional flag to control using release tags for evaluating prerelease version bumping. This is almost always the correct option since semantic-release will be creating tags for every dependency and it would lead to us bumping to a non-existent version. Set to false if you've already compensated for this in your workflow previously (true as default) 24 | --ignore-packages Packages list to be ignored on bumping process 25 | --ignore-private Exclude private packages. Enabled by default, pass 'no-ignore-private' to disable. 26 | --tag-format Format to use for creating tag names. Should include "name" and "version" vars. Default: "\${name}@\${version}" generates "package-name@1.0.0" 27 | --help Help info. 28 | 29 | Examples 30 | $ multi-semantic-release --debug 31 | $ multi-semantic-release --deps.bump=satisfy --deps.release=patch 32 | $ multi-semantic-release --ignore-packages=packages/a/**,packages/b/** 33 | `, 34 | { 35 | importMeta: import.meta, 36 | get argv() { 37 | const argvStart = process.argv.includes("--") ? process.argv.indexOf("--") + 1 : 2; 38 | return process.argv.slice(argvStart); 39 | }, 40 | booleanDefault: undefined, 41 | flags: { 42 | sequentialInit: { 43 | type: "boolean", 44 | }, 45 | sequentialPrepare: { 46 | type: "boolean", 47 | }, 48 | firstParent: { 49 | type: "boolean", 50 | }, 51 | debug: { 52 | type: "boolean", 53 | }, 54 | "deps.bump": { 55 | type: "string", 56 | }, 57 | "deps.release": { 58 | type: "string", 59 | }, 60 | "deps.prefix": { 61 | type: "string", 62 | }, 63 | "deps.pullTagsForPrerelease": { 64 | type: "boolean", 65 | }, 66 | ignorePrivate: { 67 | type: "boolean", 68 | }, 69 | ignorePackages: { 70 | type: "string", 71 | }, 72 | tagFormat: { 73 | type: "string", 74 | }, 75 | dryRun: { 76 | type: "boolean", 77 | }, 78 | silent: { 79 | type: "boolean", 80 | }, 81 | }, 82 | } 83 | ); 84 | 85 | const processFlags = (flags) => { 86 | return toPairs(flags).reduce((m, [k, v]) => { 87 | if (k === "ignorePackages" && v) { 88 | return set(m, k, v.split(",")); 89 | } 90 | 91 | // FIXME Something is wrong with the default negate parser. 92 | if (flags[`no${k[0].toUpperCase()}${k.slice(1)}`]) { 93 | flags[k] = false; 94 | return set(m, k, false); 95 | } 96 | 97 | return set(m, k, v); 98 | }, {}); 99 | }; 100 | 101 | const runner = async (cliFlags) => { 102 | // Catch errors. 103 | try { 104 | // Imports. 105 | const multiSemanticRelease = (await import("../lib/multiSemanticRelease.js")).default; 106 | 107 | // Do multirelease (log out any errors). 108 | multiSemanticRelease(null, {}, {}, cliFlags).then( 109 | () => { 110 | // Success. 111 | process.exit(0); 112 | }, 113 | (error) => { 114 | // Log out errors. 115 | logger.error(`[multi-semantic-release]:`, error); 116 | process.exit(1); 117 | } 118 | ); 119 | } catch (error) { 120 | // Log out errors. 121 | logger.error(`[multi-semantic-release]:`, error); 122 | process.exit(1); 123 | } 124 | }; 125 | 126 | runner(processFlags(cli.flags)); 127 | -------------------------------------------------------------------------------- /lib/RescopedStream.js: -------------------------------------------------------------------------------- 1 | import { Writable } from "stream"; 2 | import { check } from "./blork.js"; 3 | 4 | /** 5 | * Create a stream that passes messages through while rewriting scope. 6 | * Replaces `[semantic-release]` with a custom scope (e.g. `[my-awesome-package]`) so output makes more sense. 7 | * 8 | * @param {stream.Writable} stream The actual stream to write messages to. 9 | * @param {string} scope The string scope for the stream (instances of the text `[semantic-release]` are replaced in the stream). 10 | * @returns {stream.Writable} Object that's compatible with stream.Writable (implements a `write()` property). 11 | * 12 | * @internal 13 | */ 14 | class RescopedStream extends Writable { 15 | // Constructor. 16 | constructor(stream, scope) { 17 | super(); 18 | check(scope, "scope: string"); 19 | check(stream, "stream: stream"); 20 | this._stream = stream; 21 | this._scope = scope; 22 | } 23 | 24 | // Custom write method. 25 | write(msg) { 26 | check(msg, "msg: string"); 27 | this._stream.write(msg.replace("[semantic-release]", `[${this._scope}]`)); 28 | } 29 | } 30 | 31 | // Exports. 32 | export default RescopedStream; 33 | -------------------------------------------------------------------------------- /lib/blork.js: -------------------------------------------------------------------------------- 1 | import { existsSync, lstatSync } from "fs"; 2 | import { checker, check, add, ValueError } from "blork"; 3 | import { Writable } from "stream"; 4 | import { WritableStreamBuffer } from "stream-buffers"; 5 | 6 | // Get some checkers. 7 | const isAbsolute = checker("absolute"); 8 | 9 | // Add a directory checker. 10 | add( 11 | "directory", 12 | (v) => isAbsolute(v) && existsSync(v) && lstatSync(v).isDirectory(), 13 | "directory that exists in the filesystem" 14 | ); 15 | 16 | // Add a writable stream checker. 17 | add( 18 | "stream", 19 | // istanbul ignore next (not important) 20 | (v) => v instanceof Writable || v instanceof WritableStreamBuffer, 21 | "instance of stream.Writable or WritableStreamBuffer" 22 | ); 23 | 24 | // Exports. 25 | export { checker, check, ValueError }; 26 | -------------------------------------------------------------------------------- /lib/cleanPath.js: -------------------------------------------------------------------------------- 1 | import { normalize, isAbsolute, join } from "path"; 2 | import { check } from "./blork.js"; 3 | 4 | /** 5 | * Normalize and make a path absolute, optionally using a custom CWD. 6 | * Trims any trailing slashes from the path. 7 | * 8 | * @param {string} path The path to normalize and make absolute. 9 | * @param {string} cwd=process.cwd() The CWD to prepend to the path to make it absolute. 10 | * @returns {string} The absolute and normalized path. 11 | * 12 | * @internal 13 | */ 14 | function cleanPath(path, cwd = process.cwd()) { 15 | // Checks. 16 | check(path, "path: path"); 17 | check(cwd, "cwd: absolute"); 18 | 19 | // Normalize, absolutify, and trim trailing slashes from the path. 20 | return normalize(isAbsolute(path) ? path : join(cwd, path)).replace(/[/\\]+$/, ""); 21 | } 22 | 23 | // Exports. 24 | export default cleanPath; 25 | -------------------------------------------------------------------------------- /lib/createInlinePluginCreator.js: -------------------------------------------------------------------------------- 1 | import getCommitsFiltered from "./getCommitsFiltered.js"; 2 | import { updateManifestDeps, resolveReleaseType } from "./updateDeps.js"; 3 | import { logger } from "./logger.js"; 4 | 5 | const { debug } = logger.withScope("msr:inlinePlugin"); 6 | 7 | /** 8 | * Create an inline plugin creator for a multirelease. 9 | * This is caused once per multirelease and returns a function which should be called once per package within the release. 10 | * 11 | * @param {Package[]} packages The multi-semantic-release context. 12 | * @param {MultiContext} multiContext The multi-semantic-release context. 13 | * @param {Object} flags argv options 14 | * @returns {Function} A function that creates an inline package. 15 | * 16 | * @internal 17 | */ 18 | function createInlinePluginCreator(packages, multiContext, flags) { 19 | // Vars. 20 | const { cwd } = multiContext; 21 | 22 | /** 23 | * Create an inline plugin for an individual package in a multirelease. 24 | * This is called once per package and returns the inline plugin used for semanticRelease() 25 | * 26 | * @param {Package} pkg The package this function is being called on. 27 | * @returns {Object} A semantic-release inline plugin containing plugin step functions. 28 | * 29 | * @internal 30 | */ 31 | function createInlinePlugin(pkg) { 32 | // Vars. 33 | const { plugins, dir, name } = pkg; 34 | const debugPrefix = `[${name}]`; 35 | 36 | /** 37 | * @var {Commit[]} List of _filtered_ commits that only apply to this package. 38 | */ 39 | let commits; 40 | 41 | /** 42 | * @param {object} pluginOptions Options to configure this plugin. 43 | * @param {object} context The semantic-release context. 44 | * @returns {Promise} void 45 | * @internal 46 | */ 47 | const verifyConditions = async (pluginOptions, context) => { 48 | // Restore context for plugins that does not rely on parsed opts. 49 | Object.assign(context.options, context.options._pkgOptions); 50 | 51 | // And bind the actual logger. 52 | Object.assign(pkg.fakeLogger, context.logger); 53 | 54 | const res = await plugins.verifyConditions(context); 55 | pkg._ready = true; 56 | 57 | debug(debugPrefix, "verified conditions"); 58 | 59 | return res; 60 | }; 61 | 62 | /** 63 | * Analyze commits step. 64 | * Responsible for determining the type of the next release (major, minor or patch). If multiple plugins with a analyzeCommits step are defined, the release type will be the highest one among plugins output. 65 | * 66 | * In multirelease: Returns "patch" if the package contains references to other local packages that have changed, or null if this package references no local packages or they have not changed. 67 | * Also updates the `context.commits` setting with one returned from `getCommitsFiltered()` (which is filtered by package directory). 68 | * 69 | * @param {object} pluginOptions Options to configure this plugin. 70 | * @param {object} context The semantic-release context. 71 | * @returns {Promise} Promise that resolves when done. 72 | * 73 | * @internal 74 | */ 75 | const analyzeCommits = async (pluginOptions, context) => { 76 | const firstParentBranch = flags.firstParent ? context.branch.name : undefined; 77 | pkg._preRelease = context.branch.prerelease || null; 78 | pkg._branch = context.branch.name; 79 | 80 | // Filter commits by directory. 81 | commits = await getCommitsFiltered(cwd, dir, context.lastRelease.gitHead, firstParentBranch); 82 | 83 | // Set context.commits so analyzeCommits does correct analysis. 84 | context.commits = commits; 85 | 86 | // Set lastRelease for package from context. 87 | pkg._lastRelease = context.lastRelease; 88 | 89 | // Set nextType for package from plugins. 90 | pkg._nextType = await plugins.analyzeCommits(context); 91 | 92 | pkg._analyzed = true; 93 | 94 | // Make sure type is "patch" if the package has any deps that have been changed. 95 | pkg._nextType = resolveReleaseType(pkg, flags.deps.bump, flags.deps.release, [], flags.deps.prefix); 96 | 97 | debug(debugPrefix, "commits analyzed"); 98 | debug(debugPrefix, `release type: ${pkg._nextType}`); 99 | 100 | // Return type. 101 | return pkg._nextType; 102 | }; 103 | 104 | /** 105 | * Generate notes step (after). 106 | * Responsible for generating the content of the release note. If multiple plugins with a generateNotes step are defined, the release notes will be the result of the concatenation of each plugin output. 107 | * 108 | * In multirelease: Edit the H2 to insert the package name and add an upgrades section to the note. 109 | * We want this at the _end_ of the release note which is why it's stored in steps-after. 110 | * 111 | * Should look like: 112 | * 113 | * ## my-amazing-package [9.2.1](github.com/etc) 2018-12-01 114 | * 115 | * ### Features 116 | * 117 | * * etc 118 | * 119 | * ### Dependencies 120 | * 121 | * * **my-amazing-plugin:** upgraded to 1.2.3 122 | * * **my-other-plugin:** upgraded to 4.9.6 123 | * 124 | * @param {object} pluginOptions Options to configure this plugin. 125 | * @param {object} context The semantic-release context. 126 | * @returns {Promise} Promise that resolves to the string 127 | * 128 | * @internal 129 | */ 130 | const generateNotes = async (pluginOptions, context) => { 131 | // Set nextRelease for package. 132 | pkg._nextRelease = context.nextRelease; 133 | 134 | // Wait until all todo packages are ready to generate notes. 135 | // await waitForAll("_nextRelease", (p) => p._nextType); 136 | 137 | // Vars. 138 | const notes = []; 139 | 140 | // Set context.commits so analyzeCommits does correct analysis. 141 | // We need to redo this because context is a different instance each time. 142 | context.commits = commits; 143 | 144 | // Get subnotes and add to list. 145 | // Inject pkg name into title if it matches e.g. `# 1.0.0` or `## [1.0.1]` (as generate-release-notes does). 146 | const subs = await plugins.generateNotes(context); 147 | // istanbul ignore else (unnecessary to test) 148 | if (subs) notes.push(subs.replace(/^(#+) (\[?\d+\.\d+\.\d+\]?)/, `$1 ${name} $2`)); 149 | 150 | // If it has upgrades add an upgrades section. 151 | const upgrades = pkg.localDeps.filter((d) => d._nextRelease); 152 | if (upgrades.length) { 153 | notes.push(`### Dependencies`); 154 | const bullets = upgrades.map((d) => `* **${d.name}:** upgraded to ${d._nextRelease.version}`); 155 | notes.push(bullets.join("\n")); 156 | } 157 | 158 | debug(debugPrefix, "notes generated"); 159 | 160 | // Return the notes. 161 | return notes.join("\n\n"); 162 | }; 163 | 164 | const prepare = async (pluginOptions, context) => { 165 | updateManifestDeps(pkg); 166 | pkg._depsUpdated = true; 167 | 168 | // Set context.commits so analyzeCommits does correct analysis. 169 | // We need to redo this because context is a different instance each time. 170 | context.commits = commits; 171 | 172 | const res = await plugins.prepare(context); 173 | pkg._prepared = true; 174 | 175 | debug(debugPrefix, "prepared"); 176 | 177 | return res; 178 | }; 179 | 180 | const publish = async (pluginOptions, context) => { 181 | const res = await plugins.publish(context); 182 | pkg._published = true; 183 | debug(debugPrefix, "published"); 184 | 185 | // istanbul ignore next 186 | return res.length ? res[0] : {}; 187 | }; 188 | 189 | const inlinePlugin = { 190 | verifyConditions, 191 | analyzeCommits, 192 | generateNotes, 193 | prepare, 194 | publish, 195 | }; 196 | 197 | // Add labels for logs. 198 | Object.keys(inlinePlugin).forEach((type) => 199 | Reflect.defineProperty(inlinePlugin[type], "pluginName", { 200 | value: "Inline plugin", 201 | writable: false, 202 | enumerable: true, 203 | }) 204 | ); 205 | 206 | debug(debugPrefix, "inlinePlugin created"); 207 | 208 | return inlinePlugin; 209 | } 210 | 211 | // Return creator function. 212 | return createInlinePlugin; 213 | } 214 | 215 | // Exports. 216 | export default createInlinePluginCreator; 217 | -------------------------------------------------------------------------------- /lib/getCommitsFiltered.js: -------------------------------------------------------------------------------- 1 | import { relative, resolve } from "path"; 2 | import gitLogParser from "git-log-parser"; 3 | import getStream from "get-stream"; 4 | import { execa } from "execa"; 5 | import { check, ValueError } from "./blork.js"; 6 | import cleanPath from "./cleanPath.js"; 7 | import { logger } from "./logger.js"; 8 | 9 | const { debug } = logger.withScope("msr:commitsFilter"); 10 | 11 | /** 12 | * Retrieve the list of commits on the current branch since the commit sha associated with the last release, or all the commits of the current branch if there is no last released version. 13 | * Commits are filtered to only return those that corresponding to the package directory. 14 | * 15 | * This is achieved by using "-- my/dir/path" with `git log` — passing this into gitLogParser() with 16 | * 17 | * @param {string} cwd Absolute path of the working directory the Git repo is in. 18 | * @param {string} dir Path to the target directory to filter by. Either absolute, or relative to cwd param. 19 | * @param {string|void} lastHead The SHA of the previous release 20 | * @param {string|void} firstParentBranch first-parent to determine which merges went into master 21 | * @return {Promise>} The list of commits on the branch `branch` since the last release. 22 | */ 23 | async function getCommitsFiltered(cwd, dir, lastHead = undefined, firstParentBranch) { 24 | // Clean paths and make sure directories exist. 25 | check(cwd, "cwd: directory"); 26 | check(dir, "dir: path"); 27 | cwd = cleanPath(cwd); 28 | dir = cleanPath(dir, cwd); 29 | check(dir, "dir: directory"); 30 | check(lastHead, "lastHead: alphanumeric{40}?"); 31 | 32 | // target must be inside and different than cwd. 33 | if (dir.indexOf(cwd) !== 0) throw new ValueError("dir: Must be inside cwd", dir); 34 | if (dir === cwd) throw new ValueError("dir: Must not be equal to cwd", dir); 35 | 36 | // Get top-level Git directory as it might be higher up the tree than cwd. 37 | const root = (await execa("git", ["rev-parse", "--show-toplevel"], { cwd })).stdout; 38 | 39 | // Add correct fields to gitLogParser. 40 | Object.assign(gitLogParser.fields, { 41 | hash: "H", 42 | message: "B", 43 | gitTags: "d", 44 | committerDate: { key: "ci", type: Date }, 45 | }); 46 | 47 | // Use git-log-parser to get the commits. 48 | const relpath = relative(root, dir); 49 | const firstParentBranchFilter = firstParentBranch ? ["--first-parent", firstParentBranch] : []; 50 | const gitLogFilterQuery = [...firstParentBranchFilter, lastHead ? `${lastHead}..HEAD` : "HEAD", "--", relpath]; 51 | const stream = gitLogParser.parse({ _: gitLogFilterQuery }, { cwd, env: process.env }); 52 | const commits = await getStream.array(stream); 53 | 54 | // Trim message and tags. 55 | commits.forEach((commit) => { 56 | commit.message = commit.message.trim(); 57 | commit.gitTags = commit.gitTags.trim(); 58 | }); 59 | 60 | debug("git log filter query: %o", gitLogFilterQuery); 61 | debug("filtered commits: %O", commits); 62 | 63 | // Return the commits. 64 | return commits; 65 | } 66 | 67 | // Exports. 68 | export default getCommitsFiltered; 69 | -------------------------------------------------------------------------------- /lib/getConfig.js: -------------------------------------------------------------------------------- 1 | import { cosmiconfig } from "cosmiconfig"; 2 | 3 | // Copied from get-config.js in semantic-release 4 | const CONFIG_NAME = "release"; 5 | const CONFIG_FILES = [ 6 | "package.json", 7 | `.${CONFIG_NAME}rc`, 8 | `.${CONFIG_NAME}rc.json`, 9 | `.${CONFIG_NAME}rc.yaml`, 10 | `.${CONFIG_NAME}rc.yml`, 11 | `.${CONFIG_NAME}rc.js`, 12 | `.${CONFIG_NAME}rc.cjs`, 13 | `${CONFIG_NAME}.config.js`, 14 | `${CONFIG_NAME}.config.cjs`, 15 | ]; 16 | 17 | /** 18 | * Get the release configuration options for a given directory. 19 | * Unfortunately we've had to copy this over from semantic-release, creating unnecessary duplication. 20 | * 21 | * @param {string} cwd The directory to search. 22 | * @returns {Object} The found configuration option 23 | * 24 | * @internal 25 | */ 26 | export default async function getConfig(cwd) { 27 | // Call cosmiconfig. 28 | const config = await cosmiconfig(CONFIG_NAME, { searchPlaces: CONFIG_FILES }).search(cwd); 29 | 30 | // Return the found config or empty object. 31 | // istanbul ignore next (not important). 32 | return config ? config.config : {}; 33 | } 34 | -------------------------------------------------------------------------------- /lib/getConfigMultiSemrel.js: -------------------------------------------------------------------------------- 1 | import resolveFrom from "resolve-from"; 2 | import { cosmiconfig } from "cosmiconfig"; 3 | import { pickBy, isNil, castArray, uniq } from "lodash-es"; 4 | import { createRequire } from "node:module"; 5 | 6 | /** 7 | * @typedef {Object} DepsConfig 8 | * @property {'override' | 'satisfy' | 'inherit'} bump 9 | * @property {'patch' | 'minor' | 'major' | 'inherit'} release 10 | * @property {'^' | '~' | ''} prefix 11 | * @property {boolean} pullTagsForPrerelease 12 | */ 13 | 14 | /** 15 | * @typedef {Object} MultiReleaseConfig 16 | * @property {boolean} sequentialInit 17 | * @property {boolean} sequentialPrepare 18 | * @property {boolean} firstParent 19 | * @property {boolean} debug 20 | * @property {boolean} ignorePrivate 21 | * @property {Array} ignorePackages 22 | * @property {string} tagFormat 23 | * @property {boolean} dryRun 24 | * @property {DepsConfig} deps 25 | * @property {boolean} silent 26 | */ 27 | 28 | const CONFIG_NAME = "multi-release"; 29 | const CONFIG_FILES = [ 30 | "package.json", 31 | `.${CONFIG_NAME}rc`, 32 | `.${CONFIG_NAME}rc.json`, 33 | `.${CONFIG_NAME}rc.yaml`, 34 | `.${CONFIG_NAME}rc.yml`, 35 | `.${CONFIG_NAME}rc.js`, 36 | `.${CONFIG_NAME}rc.cjs`, 37 | `${CONFIG_NAME}.config.js`, 38 | `${CONFIG_NAME}.config.cjs`, 39 | ]; 40 | 41 | const mergeConfig = (a = {}, b = {}) => { 42 | return { 43 | ...a, 44 | // Remove `null` and `undefined` options so they can be replaced with default ones 45 | ...pickBy(b, (option) => !isNil(option)), 46 | // Treat nested objects differently as otherwise we'll loose undefined keys 47 | deps: { 48 | ...a.deps, 49 | ...pickBy(b.deps, (option) => !isNil(option)), 50 | }, 51 | // Treat arrays differently by merging them 52 | ignorePackages: uniq([...castArray(a.ignorePackages || []), ...castArray(b.ignorePackages || [])]), 53 | }; 54 | }; 55 | 56 | /** 57 | * Get the multi semantic release configuration options for a given directory. 58 | * 59 | * @param {string} cwd The directory to search. 60 | * @param {Object} cliOptions cli supplied options. 61 | * @returns {MultiReleaseConfig} The found configuration option 62 | * 63 | * @internal 64 | */ 65 | export default async function getConfig(cwd, cliOptions) { 66 | const { config } = (await cosmiconfig(CONFIG_NAME, { searchPlaces: CONFIG_FILES }).search(cwd)) || {}; 67 | const { extends: extendPaths, ...rest } = { ...config }; 68 | 69 | let options = rest; 70 | 71 | if (extendPaths) { 72 | const require = createRequire(import.meta.url); 73 | // If `extends` is defined, load and merge each shareable config 74 | const extendedOptions = castArray(extendPaths).reduce((result, extendPath) => { 75 | const extendsOptions = require(resolveFrom(cwd, extendPath)); 76 | return mergeConfig(result, extendsOptions); 77 | }, {}); 78 | 79 | options = mergeConfig(options, extendedOptions); 80 | } 81 | 82 | // Set default options values if not defined yet 83 | options = mergeConfig( 84 | { 85 | sequentialInit: false, 86 | sequentialPrepare: true, 87 | firstParent: false, 88 | debug: false, 89 | ignorePrivate: true, 90 | ignorePackages: [], 91 | tagFormat: "${name}@${version}", 92 | dryRun: undefined, 93 | deps: { 94 | bump: "override", 95 | release: "patch", 96 | prefix: "", 97 | pullTagsForPrerelease: false, 98 | }, 99 | silent: false, 100 | }, 101 | options 102 | ); 103 | 104 | // Finally merge CLI options last so they always win 105 | return mergeConfig(options, cliOptions); 106 | } 107 | -------------------------------------------------------------------------------- /lib/getConfigSemantic.js: -------------------------------------------------------------------------------- 1 | import semanticGetConfig from "semantic-release/lib/get-config.js"; 2 | import { WritableStreamBuffer } from "stream-buffers"; 3 | import { logger } from "./logger.js"; 4 | import signale from "signale"; 5 | 6 | const { Signale } = signale; 7 | 8 | /** 9 | * Get the release configuration options for a given directory. 10 | * Unfortunately we've had to copy this over from semantic-release, creating unnecessary duplication. 11 | * 12 | * @param {Object} context Object containing cwd, env, and logger properties that are passed to getConfig() 13 | * @param {Object} options Options object for the config. 14 | * @returns {Object} Returns what semantic-release's get config returns (object with options and plugins objects). 15 | * 16 | * @internal 17 | */ 18 | async function getConfigSemantic({ cwd, env, stdout, stderr }, options) { 19 | try { 20 | // Blackhole logger (so we don't clutter output with "loaded plugin" messages). 21 | const blackhole = new Signale({ stream: new WritableStreamBuffer() }); 22 | 23 | // Return semantic-release's getConfig script. 24 | return await semanticGetConfig({ cwd, env, stdout, stderr, logger: blackhole }, options); 25 | } catch (error) { 26 | // Log error and rethrow it. 27 | // istanbul ignore next (not important) 28 | logger.failure(`Error in semantic-release getConfig(): %0`, error); 29 | // istanbul ignore next (not important) 30 | throw error; 31 | } 32 | } 33 | 34 | // Exports. 35 | export default getConfigSemantic; 36 | -------------------------------------------------------------------------------- /lib/getManifest.js: -------------------------------------------------------------------------------- 1 | import { existsSync, lstatSync, readFileSync } from "fs"; 2 | 3 | /** 4 | * Read the content of target package.json if exists. 5 | * 6 | * @param {string} path file path 7 | * @returns {string} file content 8 | * 9 | * @internal 10 | */ 11 | function readManifest(path) { 12 | // Check it exists. 13 | if (!existsSync(path)) throw new ReferenceError(`package.json file not found: "${path}"`); 14 | 15 | // Stat the file. 16 | let stat; 17 | try { 18 | stat = lstatSync(path); 19 | } catch (_) { 20 | // istanbul ignore next (hard to test — happens if no read access etc). 21 | throw new ReferenceError(`package.json cannot be read: "${path}"`); 22 | } 23 | 24 | // Check it's a file! 25 | if (!stat.isFile()) throw new ReferenceError(`package.json is not a file: "${path}"`); 26 | 27 | // Read the file. 28 | try { 29 | return readFileSync(path, "utf8"); 30 | } catch (_) { 31 | // istanbul ignore next (hard to test — happens if no read access etc). 32 | throw new ReferenceError(`package.json cannot be read: "${path}"`); 33 | } 34 | } 35 | 36 | /** 37 | * Get the parsed contents of a package.json manifest file. 38 | * 39 | * @param {string} path The path to the package.json manifest file. 40 | * @returns {object} The manifest file's contents. 41 | * 42 | * @internal 43 | */ 44 | function getManifest(path) { 45 | // Read the file. 46 | const contents = readManifest(path); 47 | 48 | // Parse the file. 49 | let manifest; 50 | try { 51 | manifest = JSON.parse(contents); 52 | } catch (_) { 53 | throw new SyntaxError(`package.json could not be parsed: "${path}"`); 54 | } 55 | 56 | // Must be an object. 57 | if (typeof manifest !== "object") throw new SyntaxError(`package.json was not an object: "${path}"`); 58 | 59 | // Must have a name. 60 | if (typeof manifest.name !== "string" || !manifest.name.length) 61 | throw new SyntaxError(`Package name must be non-empty string: "${path}"`); 62 | 63 | // Check dependencies. 64 | const checkDeps = (scope) => { 65 | if (manifest.hasOwnProperty(scope) && typeof manifest[scope] !== "object") 66 | throw new SyntaxError(`Package ${scope} must be object: "${path}"`); 67 | }; 68 | 69 | checkDeps("dependencies"); 70 | checkDeps("devDependencies"); 71 | checkDeps("peerDependencies"); 72 | checkDeps("optionalDependencies"); 73 | 74 | // NOTE non-enumerable prop is skipped by JSON.stringify 75 | Object.defineProperty(manifest, "__contents__", { enumerable: false, value: contents }); 76 | 77 | // Return contents. 78 | return manifest; 79 | } 80 | 81 | // Exports. 82 | export default getManifest; 83 | -------------------------------------------------------------------------------- /lib/git.js: -------------------------------------------------------------------------------- 1 | import { execaSync } from "execa"; 2 | 3 | /** 4 | * Get all the tags for a given branch. 5 | * 6 | * @param {String} branch The branch for which to retrieve the tags. 7 | * @param {Object} [execaOptions] Options to pass to `execa`. 8 | * @param {Array} filters List of string to be checked inside tags. 9 | * 10 | * @return {Array} List of git tags. 11 | * @throws {Error} If the `git` command fails. 12 | * @internal 13 | */ 14 | function getTags(branch, execaOptions, filters) { 15 | let tags = execaSync("git", ["tag", "--merged", branch], execaOptions).stdout; 16 | tags = tags 17 | .split("\n") 18 | .map((tag) => tag.trim()) 19 | .filter(Boolean); 20 | 21 | if (!filters || !filters.length) return tags; 22 | 23 | const validateSubstr = (t, f) => f.every((v) => t.includes(v)); 24 | 25 | return tags.filter((tag) => validateSubstr(tag, filters)); 26 | } 27 | 28 | export { getTags }; 29 | -------------------------------------------------------------------------------- /lib/logger.js: -------------------------------------------------------------------------------- 1 | import dbg from "debug"; 2 | import singnale from "signale"; 3 | 4 | const { Signale } = singnale; 5 | const severityOrder = ["error", "warn", "info", "debug", "trace"]; 6 | const assertLevel = (level, limit) => severityOrder.indexOf(level) <= severityOrder.indexOf(limit); 7 | const aliases = { 8 | failure: "error", 9 | log: "info", 10 | success: "info", 11 | complete: "info", 12 | }; 13 | 14 | export const logger = { 15 | prefix: "msr:", 16 | config: { 17 | _level: "info", 18 | _stderr: process.stderr, 19 | _stdout: process.stdout, 20 | _signale: {}, 21 | set level(l) { 22 | if (!l) { 23 | return; 24 | } 25 | if (assertLevel(l, "debug")) { 26 | dbg.enable("msr:"); 27 | } 28 | if (assertLevel(l, "trace")) { 29 | dbg.enable("semantic-release:"); 30 | } 31 | this._level = l; 32 | }, 33 | get level() { 34 | return this._level; 35 | }, 36 | set stdio([stderr, stdout]) { 37 | this._stdout = stdout; 38 | this._stderr = stderr; 39 | this._signale = new Signale({ 40 | config: { displayTimestamp: true, displayLabel: false }, 41 | // scope: "multirelease", 42 | stream: stdout, 43 | types: { 44 | error: { color: "red", label: "", stream: [stderr] }, 45 | log: { color: "magenta", label: "", stream: [stdout], badge: "•" }, 46 | success: { color: "green", label: "", stream: [stdout] }, 47 | complete: { color: "green", label: "", stream: [stdout], badge: "🎉" }, 48 | }, 49 | }); 50 | }, 51 | get stdio() { 52 | return [this._stderr, this._stdout]; 53 | }, 54 | }, 55 | withScope(prefix) { 56 | return { 57 | ...this, 58 | prefix, 59 | debug: dbg(prefix || this.prefix), 60 | }; 61 | }, 62 | ...[...severityOrder, ...Object.keys(aliases)].reduce((m, l) => { 63 | m[l] = function (...args) { 64 | if (assertLevel(aliases[l] || l, this.config.level)) { 65 | (this.config._signale[l] || console[l] || (() => {}))(this.prefix, ...args); 66 | } 67 | }; 68 | return m; 69 | }, {}), 70 | debug: dbg("msr:"), 71 | }; 72 | -------------------------------------------------------------------------------- /lib/multiSemanticRelease.js: -------------------------------------------------------------------------------- 1 | import semanticRelease from "semantic-release"; 2 | import { uniq, template, sortBy } from "lodash-es"; 3 | import { topo } from "@semrel-extra/topo"; 4 | import { dirname, join } from "path"; 5 | import { check } from "./blork.js"; 6 | import { logger } from "./logger.js"; 7 | import getConfig from "./getConfig.js"; 8 | import getConfigMultiSemrel from "./getConfigMultiSemrel.js"; 9 | import getConfigSemantic from "./getConfigSemantic.js"; 10 | import getManifest from "./getManifest.js"; 11 | import cleanPath from "./cleanPath.js"; 12 | import RescopedStream from "./RescopedStream.js"; 13 | import createInlinePluginCreator from "./createInlinePluginCreator.js"; 14 | import { createRequire } from "module"; 15 | 16 | /** 17 | * The multirelease context. 18 | * @typedef MultiContext 19 | * @param {Package[]} packages Array of all packages in this multirelease. 20 | * @param {Package[]} releasing Array of packages that will release. 21 | * @param {string} cwd The current working directory. 22 | * @param {Object} env The environment variables. 23 | * @param {Logger} logger The logger for the multirelease. 24 | * @param {Stream} stdout The output stream for this multirelease. 25 | * @param {Stream} stderr The error stream for this multirelease. 26 | */ 27 | 28 | /** 29 | * Details about an individual package in a multirelease 30 | * @typedef Package 31 | * @param {string} path String path to `package.json` for the package. 32 | * @param {string} dir The working directory for the package. 33 | * @param {string} name The name of the package, e.g. `my-amazing-package` 34 | * @param {string[]} deps Array of all dependency package names for the package (merging dependencies, devDependencies, peerDependencies). 35 | * @param {Package[]} localDeps Array of local dependencies this package relies on. 36 | * @param {context|void} context The semantic-release context for this package's release (filled in once semantic-release runs). 37 | * @param {undefined|Result|false} result The result of semantic-release (object with lastRelease, nextRelease, commits, releases), false if this package was skipped (no changes or similar), or undefined if the package's release hasn't completed yet. 38 | * @param {Object} options Aggregate of semantic-release options for the package 39 | * @param {boolean} pullTagsForPrerelease if set to true, the package will use tags to determine if its dependencies need to change (legacy functionality) 40 | * @param {Object} _lastRelease The last release object for the package before its current release (set during anaylze-commit) 41 | * @param {Object} _nextRelease The next release object (the release the package is releasing for this cycle) (set during generateNotes) 42 | */ 43 | 44 | /** 45 | * Perform a multirelease. 46 | * 47 | * @param {string[]} paths An array of paths to package.json files. 48 | * @param {Object} inputOptions An object containing semantic-release options. 49 | * @param {Object} settings An object containing: cwd, env, stdout, stderr (mainly for configuring tests). 50 | * @param {Object} _flags Argv flags. 51 | * @returns {Promise} Promise that resolves to a list of package objects with `result` property describing whether it released or not. 52 | */ 53 | async function multiSemanticRelease( 54 | paths, 55 | inputOptions = {}, 56 | { cwd = process.cwd(), env = process.env, stdout = process.stdout, stderr = process.stderr } = {}, 57 | _flags 58 | ) { 59 | // Check params. 60 | if (paths) { 61 | check(paths, "paths: string[]"); 62 | } 63 | check(cwd, "cwd: directory"); 64 | check(env, "env: objectlike"); 65 | check(stdout, "stdout: stream"); 66 | check(stderr, "stderr: stream"); 67 | cwd = cleanPath(cwd); 68 | 69 | const flags = normalizeFlags(await getConfigMultiSemrel(cwd, _flags)); 70 | const require = createRequire(import.meta.url); 71 | const multisemrelPkgJson = require("../package.json"); 72 | const semrelPkgJson = require("semantic-release/package.json"); 73 | 74 | // Setup logger. 75 | logger.config.stdio = [stderr, stdout]; 76 | logger.config.level = flags.logLevel; 77 | if (flags.silent) { 78 | logger.config.level = "silent"; 79 | } 80 | if (flags.debug) { 81 | logger.config.level = "debug"; 82 | } 83 | 84 | logger.info(`multi-semantic-release version: ${multisemrelPkgJson.version}`); 85 | logger.info(`semantic-release version: ${semrelPkgJson.version}`); 86 | logger.info(`flags: ${JSON.stringify(flags, null, 2)}`); 87 | 88 | // Vars. 89 | const globalOptions = await getConfig(cwd); 90 | const multiContext = { 91 | globalOptions, 92 | inputOptions, 93 | cwd, 94 | env, 95 | stdout, 96 | stderr, 97 | pullTagsForPrerelease: flags.deps.pullTagsForPrerelease, 98 | }; 99 | const { queue, packages: _packages } = await topo({ 100 | cwd, 101 | workspacesExtra: Array.isArray(flags.ignorePackages) ? flags.ignorePackages.map((p) => `!${p}`) : [], 102 | filter: ({ manifest, manifestAbsPath, manifestRelPath }) => 103 | (!flags.ignorePrivate || !manifest.private) && 104 | (paths ? paths.includes(manifestAbsPath) || paths.includes(manifestRelPath) : true), 105 | }); 106 | 107 | // Get list of package.json paths according to workspaces. 108 | paths = paths || Object.values(_packages).map((pkg) => pkg.manifestPath); 109 | 110 | // Start. 111 | logger.complete(`Started multirelease! Loading ${paths.length} packages...`); 112 | 113 | // Load packages from paths. 114 | const packages = await Promise.all(paths.map((path) => getPackage(path, multiContext))); 115 | packages.forEach((pkg) => { 116 | // Once we load all the packages we can find their cross refs 117 | // Make a list of local dependencies. 118 | // Map dependency names (e.g. my-awesome-dep) to their actual package objects in the packages array. 119 | pkg.localDeps = uniq(pkg.deps.map((d) => packages.find((p) => d === p.name)).filter(Boolean)); 120 | 121 | logger.success(`Loaded package ${pkg.name}`); 122 | }); 123 | 124 | logger.complete(`Queued ${queue.length} packages! Starting release...`); 125 | 126 | // Release all packages. 127 | const createInlinePlugin = createInlinePluginCreator(packages, multiContext, flags); 128 | const released = await queue.reduce(async (_m, _name) => { 129 | const m = await _m; 130 | const pkg = packages.find(({ name }) => name === _name); 131 | if (pkg) { 132 | const { result } = await releasePackage(pkg, createInlinePlugin, multiContext, flags); 133 | if (result) { 134 | return m + 1; 135 | } 136 | } 137 | return m; 138 | }, Promise.resolve(0)); 139 | 140 | // Return packages list. 141 | logger.complete(`Released ${released} of ${queue.length} packages, semantically!`); 142 | 143 | return sortBy(packages, ({ name }) => queue.indexOf(name)); 144 | } 145 | 146 | /** 147 | * Load details about a package. 148 | * 149 | * @param {string} path The path to load details about. 150 | * @param {Object} allOptions Options that apply to all packages. 151 | * @param {MultiContext} multiContext Context object for the multirelease. 152 | * @returns {Promise} A package object, or void if the package was skipped. 153 | * 154 | * @internal 155 | */ 156 | async function getPackage(path, { globalOptions, inputOptions, env, cwd, stdout, stderr, pullTagsForPrerelease }) { 157 | // Make path absolute. 158 | path = cleanPath(path, cwd); 159 | const dir = dirname(path); 160 | 161 | // Get package.json file contents. 162 | const manifest = getManifest(path); 163 | const name = manifest.name; 164 | 165 | // Combine list of all dependency names. 166 | const deps = Object.keys({ 167 | ...manifest.dependencies, 168 | ...manifest.devDependencies, 169 | ...manifest.peerDependencies, 170 | ...manifest.optionalDependencies, 171 | }); 172 | 173 | // Load the package-specific options. 174 | const pkgOptions = await getConfig(dir); 175 | 176 | // The 'final options' are the global options merged with package-specific options. 177 | // We merge this ourselves because package-specific options can override global options. 178 | const finalOptions = Object.assign({}, globalOptions, pkgOptions, inputOptions); 179 | 180 | // Make a fake logger so semantic-release's get-config doesn't fail. 181 | const fakeLogger = { error() {}, log() {} }; 182 | 183 | // Use semantic-release's internal config with the final options (now we have the right `options.plugins` setting) to get the plugins object and the options including defaults. 184 | // We need this so we can call e.g. plugins.analyzeCommit() to be able to affect the input and output of the whole set of plugins. 185 | const { options, plugins } = await getConfigSemantic({ cwd: dir, env, stdout, stderr }, finalOptions); 186 | 187 | // Return package object. 188 | return { 189 | path, 190 | dir, 191 | name, 192 | manifest, 193 | deps, 194 | options, 195 | plugins, 196 | fakeLogger: fakeLogger, 197 | pullTagsForPrerelease: !!pullTagsForPrerelease, 198 | }; 199 | } 200 | 201 | /** 202 | * Release an individual package. 203 | * 204 | * @param {Package} pkg The specific package. 205 | * @param {Function} createInlinePlugin A function that creates an inline plugin. 206 | * @param {MultiContext} multiContext Context object for the multirelease. 207 | * @param {Object} flags Argv flags. 208 | * @returns {Promise} Promise that resolves when done. 209 | * 210 | * @internal 211 | */ 212 | async function releasePackage(pkg, createInlinePlugin, multiContext, flags) { 213 | // Vars. 214 | const { options: pkgOptions, name, dir } = pkg; 215 | const { env, stdout, stderr } = multiContext; 216 | 217 | // Make an 'inline plugin' for this package. 218 | // The inline plugin is the only plugin we call semanticRelease() with. 219 | // The inline plugin functions then call e.g. plugins.analyzeCommits() manually and sometimes manipulate the responses. 220 | const inlinePlugin = createInlinePlugin(pkg); 221 | 222 | // Set the options that we call semanticRelease() with. 223 | // This consists of: 224 | // - The global options (e.g. from the top level package.json) 225 | // - The package options (e.g. from the specific package's package.json) 226 | const options = { ...pkgOptions, ...inlinePlugin }; 227 | 228 | // Add the package name into tagFormat. 229 | // Thought about doing a single release for the tag (merging several packages), but it's impossible to prevent Github releasing while allowing NPM to continue. 230 | // It'd also be difficult to merge all the assets into one release without full editing/overriding the plugins. 231 | const tagFormatCtx = { 232 | name, 233 | version: "${version}", 234 | }; 235 | 236 | const tagFormatDefault = "${name}@${version}"; 237 | options.tagFormat = template(flags.tagFormat || tagFormatDefault)(tagFormatCtx); 238 | 239 | // These are the only two options that MSR shares with semrel 240 | // Set them manually for now, defaulting to the msr versions 241 | // This is approach can be reviewed if there's ever more crossover. 242 | // - debug is only supported in semrel as a CLI arg, always default to MSR 243 | options.debug = flags.debug; 244 | // - dryRun should use the msr version if specified, otherwise fallback to semrel 245 | options.dryRun = flags.dryRun === undefined ? options.dryRun : flags.dryRun; 246 | 247 | // This options are needed for plugins that do not rely on `pluginOptions` and extract them independently. 248 | options._pkgOptions = pkgOptions; 249 | 250 | // Call semanticRelease() on the directory and save result to pkg. 251 | // Don't need to log out errors as semantic-release already does that. 252 | pkg.result = await semanticRelease(options, { 253 | cwd: dir, 254 | env, 255 | stdout: new RescopedStream(stdout, name), 256 | stderr: new RescopedStream(stderr, name), 257 | }); 258 | 259 | return pkg; 260 | } 261 | 262 | function normalizeFlags(_flags) { 263 | return { 264 | deps: { 265 | pullTagsForPrerelease: !!_flags.deps?.pullTagsForPrerelease, 266 | }, 267 | ..._flags, 268 | }; 269 | } 270 | 271 | // Exports. 272 | export default multiSemanticRelease; 273 | -------------------------------------------------------------------------------- /lib/recognizeFormat.js: -------------------------------------------------------------------------------- 1 | import { detectNewline } from "detect-newline"; 2 | import detectIndent from "detect-indent"; 3 | 4 | /** 5 | * Information about the format of a file. 6 | * @typedef FileFormat 7 | * @property {string|number} indent Indentation characters 8 | * @property {string} trailingWhitespace Trailing whitespace at the end of the file 9 | */ 10 | 11 | /** 12 | * Detects the indentation and trailing whitespace of a file. 13 | * 14 | * @param {string} contents contents of the file 15 | * @returns {FileFormat} Formatting of the file 16 | */ 17 | function recognizeFormat(contents) { 18 | return { 19 | indent: detectIndent(contents).indent, 20 | trailingWhitespace: detectNewline(contents) || "", 21 | }; 22 | } 23 | 24 | // Exports. 25 | export default recognizeFormat; 26 | -------------------------------------------------------------------------------- /lib/updateDeps.js: -------------------------------------------------------------------------------- 1 | import { writeFileSync } from "fs"; 2 | import semver from "semver"; 3 | import { isObject, isEqual, transform } from "lodash-es"; 4 | import recognizeFormat from "./recognizeFormat.js"; 5 | import getManifest from "./getManifest.js"; 6 | import { getHighestVersion, getLatestVersion } from "./utils.js"; 7 | import { getTags } from "./git.js"; 8 | import { logger } from "./logger.js"; 9 | 10 | const { debug } = logger.withScope("msr:updateDeps"); 11 | 12 | /** 13 | * Resolve next package version. 14 | * 15 | * @param {Package} pkg Package object. 16 | * @returns {string|undefined} Next pkg version. 17 | * @internal 18 | */ 19 | const getNextVersion = (pkg) => { 20 | const lastVersion = pkg._lastRelease && pkg._lastRelease.version; 21 | 22 | return lastVersion && typeof pkg._nextType === "string" 23 | ? semver.inc(lastVersion, pkg._nextType) 24 | : lastVersion || "1.0.0"; 25 | }; 26 | 27 | /** 28 | * Resolve the package version from a tag 29 | * 30 | * @param {Package} pkg Package object. 31 | * @param {string} tag The tag containing the version to resolve 32 | * @returns {string|null} The version of the package or null if no tag was passed 33 | * @internal 34 | */ 35 | const getVersionFromTag = (pkg, tag) => { 36 | if (!pkg.name) return tag || null; 37 | if (!tag) return null; 38 | 39 | // TODO inherit semantic-release/lib/branches/get-tags.js 40 | const strMatch = tag.match(/[0-9].[0-9].[0-9][^+]*/); 41 | return strMatch && strMatch[0] && semver.valid(strMatch[0]) ? strMatch[0] : null; 42 | }; 43 | 44 | /** 45 | * Resolve next package version on prereleases. 46 | * 47 | * Will resolve highest next version of either: 48 | * 49 | * 1. The last release for the package during this multi-release cycle 50 | * 2. (if the package has pullTagsForPrerelease true): 51 | * a. the highest increment of the tags array provided 52 | * b. the highest increment of the gitTags for the prerelease 53 | * 54 | * @param {Package} pkg Package object. 55 | * @param {Array} tags Override list of tags from specific pkg and branch. 56 | * @returns {string|undefined} Next pkg version. 57 | * @internal 58 | */ 59 | const getNextPreVersion = (pkg, tags) => { 60 | const tagFilters = [pkg._preRelease]; 61 | // Note: this is only set is a current multi-semantic-release released 62 | const lastVersionForCurrentRelease = pkg._lastRelease && pkg._lastRelease.version; 63 | 64 | if (!pkg.pullTagsForPrerelease && tags) { 65 | throw new Error("Supplied tags for NextPreVersion but the package does not use tags for next prerelease"); 66 | } 67 | 68 | // Extract tags: 69 | // 1. Set filter to extract only package tags 70 | // 2. Get tags from a branch considering the filters established 71 | // 3. Resolve the versions from the tags 72 | // TODO: replace {cwd: '.'} with multiContext.cwd 73 | if (pkg.name) tagFilters.push(pkg.name); 74 | if (!tags && pkg.pullTagsForPrerelease) { 75 | try { 76 | tags = getTags(pkg._branch, { cwd: pkg.dir }, tagFilters); 77 | } catch (e) { 78 | tags = []; 79 | console.warn(e); 80 | console.warn(`Try 'git pull ${pkg._branch}'`); 81 | } 82 | } 83 | 84 | const lastPreRelTag = getPreReleaseTag(lastVersionForCurrentRelease); 85 | const isNewPreRelTag = lastPreRelTag && lastPreRelTag !== pkg._preRelease; 86 | 87 | const versionToSet = 88 | isNewPreRelTag || !lastVersionForCurrentRelease 89 | ? `1.0.0-${pkg._preRelease}.1` 90 | : _nextPreVersionCases( 91 | tags ? tags.map((tag) => getVersionFromTag(pkg, tag)).filter((tag) => tag) : [], 92 | lastVersionForCurrentRelease, 93 | pkg._nextType, 94 | pkg._preRelease 95 | ); 96 | 97 | return versionToSet; 98 | }; 99 | 100 | /** 101 | * Parse the prerelease tag from a semver version. 102 | * 103 | * @param {string} version Semver version in a string format. 104 | * @returns {string|null} preReleaseTag Version prerelease tag or null. 105 | * @internal 106 | */ 107 | const getPreReleaseTag = (version) => { 108 | const parsed = semver.parse(version); 109 | if (!parsed) return null; 110 | return parsed.prerelease[0] || null; 111 | }; 112 | 113 | /** 114 | * Resolve next prerelease special cases: highest version from tags or major/minor/patch. 115 | * 116 | * @param {Array} tags - if non-empty, we will use these tags as part fo the comparison 117 | * @param {string} lastVersionForCurrentMultiRelease Last package version released from multi-semantic-release 118 | * @param {string} pkgNextType Next type evaluated for the next package type. 119 | * @param {string} pkgPreRelease Package prerelease suffix. 120 | * @returns {string|undefined} Next pkg version. 121 | * @internal 122 | */ 123 | const _nextPreVersionCases = (tags, lastVersionForCurrentMultiRelease, pkgNextType, pkgPreRelease) => { 124 | // Case 1: Normal release on last version and is now converted to a prerelease 125 | if (!semver.prerelease(lastVersionForCurrentMultiRelease)) { 126 | const { major, minor, patch } = semver.parse(lastVersionForCurrentMultiRelease); 127 | return `${semver.inc(`${major}.${minor}.${patch}`, pkgNextType || "patch")}-${pkgPreRelease}.1`; 128 | } 129 | 130 | // Case 2: Validates version with tags 131 | const latestTag = getLatestVersion(tags, { withPrerelease: true }); 132 | return _nextPreHighestVersion(latestTag, lastVersionForCurrentMultiRelease, pkgPreRelease); 133 | }; 134 | 135 | /** 136 | * Resolve next prerelease comparing bumped tags versions with last version. 137 | * 138 | * @param {string|null} latestTag Last released tag from branch or null if non-existent. 139 | * @param {string} lastVersion Last version released. 140 | * @param {string} pkgPreRelease Prerelease tag from package to-be-released. 141 | * @returns {string} Next pkg version. 142 | * @internal 143 | */ 144 | const _nextPreHighestVersion = (latestTag, lastVersion, pkgPreRelease) => { 145 | const bumpFromTags = latestTag ? semver.inc(latestTag, "prerelease", pkgPreRelease) : null; 146 | const bumpFromLast = semver.inc(lastVersion, "prerelease", pkgPreRelease); 147 | 148 | return bumpFromTags ? getHighestVersion(bumpFromLast, bumpFromTags) : bumpFromLast; 149 | }; 150 | 151 | /** 152 | * Resolve package release type taking into account the cascading dependency update. 153 | * 154 | * @param {Package} pkg Package object. 155 | * @param {string|undefined} bumpStrategy Dependency resolution strategy: override, satisfy, inherit. 156 | * @param {string|undefined} releaseStrategy Release type triggered by deps updating: patch, minor, major, inherit. 157 | * @param {Package[]} ignore=[] Packages to ignore (to prevent infinite loops). 158 | * @param {string} prefix Dependency version prefix to be attached if `bumpStrategy='override'`. ^ | ~ | '' (defaults to empty string) 159 | * @returns {string|undefined} Resolved release type. 160 | * @internal 161 | */ 162 | const resolveReleaseType = (pkg, bumpStrategy = "override", releaseStrategy = "patch", ignore = [], prefix = "") => { 163 | // NOTE This fn also updates pkg deps, so it must be invoked anyway. 164 | const dependentReleaseType = getDependentRelease(pkg, bumpStrategy, releaseStrategy, ignore, prefix); 165 | 166 | // Define release type for dependent package if any of its deps changes. 167 | // `patch`, `minor`, `major` — strictly declare the release type that occurs when any dependency is updated. 168 | // `inherit` — applies the "highest" release of updated deps to the package. 169 | // For example, if any dep has a breaking change, `major` release will be applied to the all dependants up the chain. 170 | // If we want to inherit the release type from the dependent package 171 | const types = ["patch", "minor", "major"]; 172 | const depIndex = dependentReleaseType ? types.indexOf(dependentReleaseType) : types.length + 1; 173 | const pkgIndex = pkg._nextType ? types.indexOf(pkg._nextType) : types.length + 1; 174 | 175 | if (releaseStrategy === "inherit" && dependentReleaseType && depIndex >= pkgIndex) { 176 | return dependentReleaseType; 177 | } 178 | 179 | // Release type found by commitAnalyzer. 180 | if (pkg._nextType) { 181 | return pkg._nextType; 182 | } 183 | 184 | // No deps changed. 185 | if (!dependentReleaseType) { 186 | return undefined; 187 | } 188 | 189 | pkg._nextType = releaseStrategy === "inherit" ? dependentReleaseType : releaseStrategy; 190 | 191 | return pkg._nextType; 192 | }; 193 | 194 | /** 195 | * Get dependent release type by recursive scanning and updating pkg deps. 196 | * 197 | * @param {Package} pkg The package with local deps to check. 198 | * @param {string} bumpStrategy Dependency resolution strategy: override, satisfy, inherit. 199 | * @param {string} releaseStrategy Release type triggered by deps updating: patch, minor, major, inherit. 200 | * @param {Package[]} ignore Packages to ignore (to prevent infinite loops). 201 | * @param {string} prefix Dependency version prefix to be attached if `bumpStrategy='override'`. ^ | ~ | '' (defaults to empty string) 202 | * @returns {string|undefined} Returns the highest release type if found, undefined otherwise 203 | * @internal 204 | */ 205 | const getDependentRelease = (pkg, bumpStrategy, releaseStrategy, ignore, prefix) => { 206 | const severityOrder = ["patch", "minor", "major"]; 207 | const { localDeps, manifest = {} } = pkg; 208 | const lastVersion = pkg._lastRelease && pkg._lastRelease.version; 209 | const { dependencies = {}, devDependencies = {}, peerDependencies = {}, optionalDependencies = {} } = manifest; 210 | const scopes = [dependencies, devDependencies, peerDependencies, optionalDependencies]; 211 | const bumpDependency = (scope, name, nextVersion) => { 212 | const currentVersion = scope[name]; 213 | if (!nextVersion || !currentVersion) { 214 | return false; 215 | } 216 | 217 | const resolvedVersion = resolveNextVersion(currentVersion, nextVersion, bumpStrategy, prefix); 218 | if (currentVersion !== resolvedVersion) { 219 | scope[name] = resolvedVersion; 220 | return true; 221 | } 222 | 223 | return false; 224 | }; 225 | 226 | // prettier-ignore 227 | return localDeps 228 | .filter((p) => !ignore.includes(p)) 229 | .reduce((releaseType, p) => { 230 | // Has changed if... 231 | // 1. Any local dep package itself has changed 232 | // 2. Any local dep package has local deps that have changed. 233 | const nextType = resolveReleaseType(p, bumpStrategy, releaseStrategy,[...ignore, pkg], prefix); 234 | const nextVersion = 235 | nextType 236 | // Update the nextVersion only if there is a next type to be bumped 237 | ? p._preRelease ? getNextPreVersion(p) : getNextVersion(p) 238 | // Set the nextVersion fallback to the last local dependency package last version 239 | : p._lastRelease && p._lastRelease.version 240 | 241 | // 3. And this change should correspond to the manifest updating rule. 242 | const requireRelease = scopes 243 | .reduce((res, scope) => bumpDependency(scope, p.name, nextVersion) || res, !lastVersion) 244 | return requireRelease && (severityOrder.indexOf(nextType) > severityOrder.indexOf(releaseType)) 245 | ? nextType 246 | : releaseType; 247 | }, undefined); 248 | }; 249 | 250 | /** 251 | * Resolve next version of dependency. 252 | * 253 | * @param {string} currentVersion Current dep version 254 | * @param {string} nextVersion Next release type: patch, minor, major 255 | * @param {string|undefined} strategy Resolution strategy: inherit, override, satisfy 256 | * @param {string} prefix Dependency version prefix to be attached if `bumpStrategy='override'`. ^ | ~ | '' (defaults to empty string) 257 | * @returns {string} Next dependency version 258 | * @internal 259 | */ 260 | const resolveNextVersion = (currentVersion, nextVersion, strategy = "override", prefix = "") => { 261 | // Check the next pkg version against its current references. 262 | // If it matches (`*` matches to any, `1.1.0` matches `1.1.x`, `1.5.0` matches to `^1.0.0` and so on) 263 | // release will not be triggered, if not `override` strategy will be applied instead. 264 | if ((strategy === "satisfy" || strategy === "inherit") && semver.satisfies(nextVersion, currentVersion)) { 265 | return currentVersion; 266 | } 267 | 268 | // `inherit` will try to follow the current declaration version/range. 269 | // `~1.0.0` + `minor` turns into `~1.1.0`, `1.x` + `major` gives `2.x`, 270 | // but `1.x` + `minor` gives `1.x` so there will be no release, etc. 271 | if (strategy === "inherit") { 272 | const sep = "."; 273 | const nextChunks = nextVersion.split(sep); 274 | const currentChunks = currentVersion.split(sep); 275 | // prettier-ignore 276 | const resolvedChunks = currentChunks.map((chunk, i) => 277 | nextChunks[i] 278 | ? chunk.replace(/\d+/, nextChunks[i]) 279 | : chunk 280 | ); 281 | 282 | return resolvedChunks.join(sep); 283 | } 284 | 285 | // "override" 286 | // By default next package version would be set as is for the all dependants. 287 | return prefix + nextVersion; 288 | }; 289 | 290 | /** 291 | * Update pkg deps. 292 | * 293 | * @param {Package} pkg The package this function is being called on. 294 | * @returns {undefined} 295 | * @internal 296 | */ 297 | const updateManifestDeps = (pkg) => { 298 | const { manifest, path } = pkg; 299 | const { indent, trailingWhitespace } = recognizeFormat(manifest.__contents__); 300 | 301 | // We need to bump pkg.version for correct yarn.lock update 302 | // https://github.com/qiwi/multi-semantic-release/issues/58 303 | manifest.version = pkg._nextRelease.version || manifest.version; 304 | 305 | // Loop through localDeps to verify release consistency. 306 | pkg.localDeps.forEach((d) => { 307 | // Get version of dependency. 308 | const release = d._nextRelease || d._lastRelease; 309 | 310 | // Cannot establish version. 311 | if (!release || !release.version) 312 | throw Error(`Cannot release ${pkg.name} because dependency ${d.name} has not been released yet`); 313 | }); 314 | 315 | if (!auditManifestChanges(manifest, path)) { 316 | return; 317 | } 318 | 319 | // Write package.json back out. 320 | writeFileSync(path, JSON.stringify(manifest, null, indent) + trailingWhitespace); 321 | }; 322 | 323 | // https://gist.github.com/Yimiprod/7ee176597fef230d1451 324 | const difference = (object, base) => 325 | transform(object, (result, value, key) => { 326 | if (!isEqual(value, base[key])) { 327 | result[key] = 328 | isObject(value) && isObject(base[key]) ? difference(value, base[key]) : `${base[key]} → ${value}`; 329 | } 330 | }); 331 | 332 | /** 333 | * Clarify what exactly was changed in manifest file. 334 | * @param {object} actualManifest manifest object 335 | * @param {string} path manifest path 336 | * @returns {boolean} has changed or not 337 | * @internal 338 | */ 339 | const auditManifestChanges = (actualManifest, path) => { 340 | const debugPrefix = `[${actualManifest.name}]`; 341 | const oldManifest = getManifest(path); 342 | const depScopes = ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"]; 343 | const changes = depScopes.reduce((res, scope) => { 344 | const diff = difference(actualManifest[scope], oldManifest[scope]); 345 | 346 | if (Object.keys(diff).length) { 347 | res[scope] = diff; 348 | } 349 | 350 | return res; 351 | }, {}); 352 | 353 | debug(debugPrefix, "package.json path=", path); 354 | 355 | if (Object.keys(changes).length) { 356 | debug(debugPrefix, "changes=", changes); 357 | return true; 358 | } 359 | 360 | debug(debugPrefix, "no deps changes"); 361 | return false; 362 | }; 363 | 364 | export { 365 | getNextVersion, 366 | getNextPreVersion, 367 | getPreReleaseTag, 368 | updateManifestDeps, 369 | resolveReleaseType, 370 | resolveNextVersion, 371 | getVersionFromTag, 372 | }; 373 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Lifted and tweaked from semantic-release because we follow how they bump their packages/dependencies. 3 | * https://github.com/semantic-release/semantic-release/blob/master/lib/utils.js 4 | */ 5 | 6 | import semver from "semver"; 7 | const { gt, lt, prerelease, rcompare } = semver; 8 | 9 | /** 10 | * Get tag objects and convert them to a list of stringified versions. 11 | * @param {array} tags Tags as object list. 12 | * @returns {array} Tags as string list. 13 | * @internal 14 | */ 15 | function tagsToVersions(tags) { 16 | if (!tags) return []; 17 | return tags.map(({ version }) => version); 18 | } 19 | 20 | /** 21 | * HOC that applies highest/lowest semver function. 22 | * @param {Function} predicate High order function to be called. 23 | * @param {string|undefined} version1 Version 1 to be compared with. 24 | * @param {string|undefined} version2 Version 2 to be compared with. 25 | * @returns {string|undefined} Highest or lowest version. 26 | * @internal 27 | */ 28 | const _selectVersionBy = (predicate, version1, version2) => { 29 | if (predicate && version1 && version2) { 30 | return predicate(version1, version2) ? version1 : version2; 31 | } 32 | return version1 || version2; 33 | }; 34 | 35 | /** 36 | * Gets highest semver function binding gt to the HOC selectVersionBy. 37 | */ 38 | const getHighestVersion = _selectVersionBy.bind(null, gt); 39 | 40 | /** 41 | * Gets lowest semver function binding gt to the HOC selectVersionBy. 42 | */ 43 | const getLowestVersion = _selectVersionBy.bind(null, lt); 44 | 45 | /** 46 | * Retrieve the latest version from a list of versions. 47 | * @param {array} versions Versions as string list. 48 | * @param {bool|undefined} withPrerelease Prerelease flag. 49 | * @returns {string|undefined} Latest version. 50 | * @internal 51 | */ 52 | function getLatestVersion(versions, withPrerelease) { 53 | return versions.filter((version) => withPrerelease || !prerelease(version)).sort(rcompare)[0]; 54 | } 55 | 56 | // https://github.com/sindresorhus/slash/blob/b5cdd12272f94cfc37c01ac9c2b4e22973e258e5/index.js#L1 57 | function slash(path) { 58 | const isExtendedLengthPath = /^\\\\\?\\/.test(path); 59 | const hasNonAscii = /[^\u0000-\u0080]+/.test(path); // eslint-disable-line no-control-regex 60 | 61 | if (isExtendedLengthPath || hasNonAscii) { 62 | return path; 63 | } 64 | 65 | return path.replace(/\\/g, "/"); 66 | } 67 | 68 | export { tagsToVersions, getHighestVersion, getLowestVersion, getLatestVersion, slash }; 69 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@qiwi/multi-semantic-release", 3 | "author": "Dave Houlbrooke ", 4 | "version": "7.1.2", 5 | "license": "0BSD", 6 | "engines": { 7 | "node": ">=14", 8 | "yarn": ">=1.0.0" 9 | }, 10 | "type": "module", 11 | "exports": { 12 | ".": "./lib/multiSemanticRelease.js", 13 | "./*": "./*" 14 | }, 15 | "bin": { 16 | "multi-semantic-release": "./bin/cli.js" 17 | }, 18 | "files": [ 19 | "README.md", 20 | "CHANGELOG.md", 21 | "lib", 22 | "bin" 23 | ], 24 | "scripts": { 25 | "watch": "jest --watchAll", 26 | "lint": "eslint ./", 27 | "lint:fix": "eslint --fix ./", 28 | "test": "yarn lint && yarn test:unit", 29 | "test:unit": "NODE_OPTIONS=\"${NODE_OPTIONS} --experimental-vm-modules\" ./node_modules/.bin/jest --coverage", 30 | "build": "echo 'There is no need for build' && exit 0", 31 | "postupdate": "yarn && npx yarn-audit-fix && yarn build && yarn test", 32 | "publish:beta": "npm publish --no-git-tag-version --tag beta" 33 | }, 34 | "commitlint": { 35 | "extends": [ 36 | "@commitlint/config-conventional" 37 | ] 38 | }, 39 | "jest": { 40 | "testEnvironmentOptions": { 41 | "url": "http://localhost" 42 | }, 43 | "collectCoverage": true, 44 | "collectCoverageFrom": [ 45 | "lib/**/*.js" 46 | ], 47 | "modulePathIgnorePatterns": [ 48 | "/test/fixtures" 49 | ], 50 | "testTimeout": 50000 51 | }, 52 | "dependencies": { 53 | "@semrel-extra/topo": "^1.14.0", 54 | "blork": "^9.3.0", 55 | "cosmiconfig": "^8.3.6", 56 | "debug": "^4.3.4", 57 | "detect-indent": "^7.0.1", 58 | "detect-newline": "^4.0.1", 59 | "execa": "^7.1.1", 60 | "get-stream": "^6.0.1", 61 | "git-log-parser": "^1.2.0", 62 | "lodash-es": "^4.17.21", 63 | "meow": "^12.0.1", 64 | "promise-events": "^0.2.4", 65 | "resolve-from": "^5.0.0", 66 | "semantic-release": "^21.0.5", 67 | "semver": "^7.5.3", 68 | "signale": "^1.4.0", 69 | "stream-buffers": "^3.0.2" 70 | }, 71 | "devDependencies": { 72 | "@commitlint/config-conventional": "^17.6.6", 73 | "@jest/globals": "^29.5.0", 74 | "@semantic-release/changelog": "^6.0.3", 75 | "@semantic-release/git": "^10.0.1", 76 | "@semantic-release/github": "^9.0.3", 77 | "@semantic-release/npm": "^10.0.4", 78 | "commitlint": "^17.6.6", 79 | "eslint": "^8.43.0", 80 | "eslint-config-prettier": "^8.8.0", 81 | "eslint-plugin-prettier": "^4.2.1", 82 | "file-url": "^4.0.0", 83 | "jest": "^29.5.0", 84 | "prettier": "^2.8.8", 85 | "tempy": "^3.0.0" 86 | }, 87 | "release": { 88 | "branch": "master", 89 | "verifyConditions": [ 90 | "@semantic-release/changelog", 91 | "@semantic-release/npm", 92 | "@semantic-release/git" 93 | ], 94 | "prepare": [ 95 | "@semantic-release/changelog", 96 | "@semantic-release/npm", 97 | "@semantic-release/git" 98 | ], 99 | "publish": [ 100 | "@semantic-release/npm", 101 | "@semantic-release/github" 102 | ] 103 | }, 104 | "repository": { 105 | "type": "git", 106 | "url": "https://github.com/qiwi/multi-semantic-release.git" 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /test/bin/cli.test.js: -------------------------------------------------------------------------------- 1 | import { execa } from "execa"; 2 | import { dirname, resolve } from "node:path"; 3 | import { fileURLToPath } from "node:url"; 4 | 5 | import { copyDirectory } from "../helpers/file.js"; 6 | import { 7 | gitInit, 8 | gitAdd, 9 | gitCommit, 10 | gitCommitAll, 11 | gitInitOrigin, 12 | gitPush, 13 | gitTag, 14 | gitGetTags, 15 | } from "../helpers/git.js"; 16 | 17 | const __dirname = dirname(fileURLToPath(import.meta.url)); 18 | const msrBin = resolve(__dirname, "../../bin/cli.js"); 19 | const env = { 20 | PATH: process.env.PATH, 21 | }; 22 | 23 | // Tests. 24 | describe("multi-semantic-release CLI", () => { 25 | test("Initial commit (changes in all packages)", async () => { 26 | // Create Git repo with copy of Yarn workspaces fixture. 27 | const cwd = gitInit(); 28 | copyDirectory(`test/fixtures/yarnWorkspaces/`, cwd); 29 | const sha = gitCommitAll(cwd, "feat: Initial release"); 30 | const url = gitInitOrigin(cwd); 31 | gitPush(cwd); 32 | 33 | // Run via command line. 34 | // const out = (await execa("node", [filepath, "-- --no-sequential-prepare"], { cwd })).stdout; 35 | // expect(out).toMatch("Started multirelease! Loading 4 packages..."); 36 | // expect(out).toMatch("Released 4 of 4 packages, semantically!"); 37 | 38 | try { 39 | await execa("node", [msrBin, "-- --no-sequential-prepare"], { cwd, extendEnv: false, env }); 40 | } catch (res) { 41 | const { stdout, stderr, exitCode } = res; 42 | 43 | expect(stdout).toMatch("Started multirelease! Loading 4 packages..."); 44 | expect(stderr).toMatch('Error: Cyclic dependency, node was:"msr-test-c"'); 45 | expect(exitCode).toBe(1); 46 | } 47 | }); 48 | 49 | test("Initial commit (changes in 2 packages, 2 filtered out)", async () => { 50 | // Create Git repo with copy of Yarn workspaces fixture. 51 | const cwd = gitInit(); 52 | copyDirectory(`test/fixtures/yarnWorkspaces/`, cwd); 53 | const sha = gitCommitAll(cwd, "feat: Initial release"); 54 | const url = gitInitOrigin(cwd); 55 | gitPush(cwd); 56 | 57 | // Run via command line. 58 | const out = ( 59 | await execa("node", [msrBin, "-- --ignore-packages=packages/c/**,packages/d/**"], { 60 | cwd, 61 | extendEnv: false, 62 | env, 63 | }) 64 | ).stdout; 65 | expect(out).toMatch("Started multirelease! Loading 2 packages..."); 66 | expect(out).toMatch("Released 2 of 2 packages, semantically!"); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /test/fixtures/badDepsPackage.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bad-deps-package", 3 | "version": "0.0.0", 4 | "dependencies": "NOPE" 5 | } -------------------------------------------------------------------------------- /test/fixtures/badDevDepsPackage.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bad-dev-deps-package", 3 | "version": "0.0.0", 4 | "devDependencies": 123 5 | } -------------------------------------------------------------------------------- /test/fixtures/badNamePackage.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.0", 3 | "devDependencies": { 4 | "multi-semantic-release-test-a": "*" 5 | } 6 | } -------------------------------------------------------------------------------- /test/fixtures/badPeerDepsPackage.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bad-dev-deps-package", 3 | "version": "0.0.0", 4 | "peerDependencies": false 5 | } -------------------------------------------------------------------------------- /test/fixtures/badYarnWorkspaces/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msr-test-yarn-bad", 3 | "author": "Dave Houlbrooke =8.3" 9 | }, 10 | "workspaces": "NOT ARRAY OF STRINGS" 11 | } -------------------------------------------------------------------------------- /test/fixtures/boltWorkspaces/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msr-test-bolt", 3 | "author": "Dave Houlbrooke ", 4 | "version": "0.0.0-semantically-released", 5 | "private": true, 6 | "license": "0BSD", 7 | "engines": { 8 | "node": ">=8.3" 9 | }, 10 | "bolt": { 11 | "workspaces": [ 12 | "packages/*" 13 | ] 14 | }, 15 | "release": { 16 | "plugins": [ 17 | "@semantic-release/commit-analyzer", 18 | "@semantic-release/release-notes-generator" 19 | ], 20 | "noCi": true 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /test/fixtures/boltWorkspaces/packages/a/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msr-test-a", 3 | "version": "0.0.0", 4 | "peerDependencies": { 5 | "left-pad": "latest" 6 | } 7 | } -------------------------------------------------------------------------------- /test/fixtures/boltWorkspaces/packages/b/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msr-test-b", 3 | "version": "0.0.0", 4 | "dependencies": { 5 | "msr-test-a": "*" 6 | }, 7 | "devDependencies": { 8 | "msr-test-d": "*", 9 | "left-pad": "latest" 10 | } 11 | } -------------------------------------------------------------------------------- /test/fixtures/boltWorkspaces/packages/c/.releaserc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tagFormat": "multi-semantic-release-test-c@v${version}" 3 | } -------------------------------------------------------------------------------- /test/fixtures/boltWorkspaces/packages/c/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msr-test-c", 3 | "version": "0.0.0", 4 | "devDependencies": { 5 | "msr-test-b": "*", 6 | "msr-test-d": "*" 7 | } 8 | } -------------------------------------------------------------------------------- /test/fixtures/boltWorkspaces/packages/d/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msr-test-d", 3 | "version": "0.0.0" 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/boltWorkspacesIgnore/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msr-test-bolt", 3 | "author": "Dave Houlbrooke =8.3" 9 | }, 10 | "bolt": { 11 | "workspaces": [ 12 | "packages/*", 13 | "!packages/d/**" 14 | ] 15 | }, 16 | "release": { 17 | "plugins": [ 18 | "@semantic-release/commit-analyzer", 19 | "@semantic-release/release-notes-generator" 20 | ], 21 | "noCi": true 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /test/fixtures/boltWorkspacesIgnore/packages/a/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msr-test-a", 3 | "version": "0.0.0", 4 | "peerDependencies": { 5 | "left-pad": "latest" 6 | } 7 | } -------------------------------------------------------------------------------- /test/fixtures/boltWorkspacesIgnore/packages/b/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msr-test-b", 3 | "version": "0.0.0", 4 | "dependencies": { 5 | "msr-test-a": "*" 6 | }, 7 | "devDependencies": { 8 | "msr-test-d": "*", 9 | "left-pad": "latest" 10 | } 11 | } -------------------------------------------------------------------------------- /test/fixtures/boltWorkspacesIgnore/packages/c/.releaserc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tagFormat": "multi-semantic-release-test-c@v${version}" 3 | } -------------------------------------------------------------------------------- /test/fixtures/boltWorkspacesIgnore/packages/c/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msr-test-c", 3 | "version": "0.0.0", 4 | "devDependencies": { 5 | "msr-test-b": "*", 6 | "msr-test-d": "*" 7 | } 8 | } -------------------------------------------------------------------------------- /test/fixtures/boltWorkspacesIgnore/packages/d/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msr-test-d", 3 | "version": "0.0.0" 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/boltWorkspacesUndefined/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msr-test-bolt", 3 | "author": "Dave Houlbrooke ", 4 | "version": "0.0.0-semantically-released", 5 | "private": true, 6 | "license": "0BSD", 7 | "engines": { 8 | "node": ">=8.3" 9 | }, 10 | "bolt": { 11 | }, 12 | "release": { 13 | "plugins": [ 14 | "@semantic-release/commit-analyzer", 15 | "@semantic-release/release-notes-generator" 16 | ], 17 | "noCi": true 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /test/fixtures/boltWorkspacesUndefined/packages/a/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msr-test-a", 3 | "version": "0.0.0", 4 | "peerDependencies": { 5 | "left-pad": "latest" 6 | } 7 | } -------------------------------------------------------------------------------- /test/fixtures/boltWorkspacesUndefined/packages/b/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msr-test-b", 3 | "version": "0.0.0", 4 | "dependencies": { 5 | "msr-test-a": "*" 6 | }, 7 | "devDependencies": { 8 | "msr-test-d": "*", 9 | "left-pad": "latest" 10 | } 11 | } -------------------------------------------------------------------------------- /test/fixtures/boltWorkspacesUndefined/packages/c/.releaserc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tagFormat": "multi-semantic-release-test-c@v${version}" 3 | } -------------------------------------------------------------------------------- /test/fixtures/boltWorkspacesUndefined/packages/c/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msr-test-c", 3 | "version": "0.0.0", 4 | "devDependencies": { 5 | "msr-test-b": "*", 6 | "msr-test-d": "*" 7 | } 8 | } -------------------------------------------------------------------------------- /test/fixtures/boltWorkspacesUndefined/packages/d/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msr-test-d", 3 | "version": "0.0.0" 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/emptyYarnWorkspaces/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msr-test-yarn-empty", 3 | "author": "Dave Houlbrooke =8.3" 9 | }, 10 | "workspaces": [ 11 | "myworkspaces" 12 | ] 13 | } -------------------------------------------------------------------------------- /test/fixtures/invalidPackage.json: -------------------------------------------------------------------------------- 1 | NOTVALIDJSON -------------------------------------------------------------------------------- /test/fixtures/numberPackage.json: -------------------------------------------------------------------------------- 1 | 123 -------------------------------------------------------------------------------- /test/fixtures/pnpmWorkspace/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msr-test-pnpm", 3 | "author": "Dave Houlbrooke ", 4 | "version": "0.0.0-semantically-released", 5 | "private": true, 6 | "license": "0BSD", 7 | "engines": { 8 | "node": ">=8.3" 9 | }, 10 | "release": { 11 | "plugins": [ 12 | "@semantic-release/commit-analyzer", 13 | "@semantic-release/release-notes-generator" 14 | ], 15 | "noCi": true 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/fixtures/pnpmWorkspace/packages/a/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msr-test-a", 3 | "version": "0.0.0", 4 | "peerDependencies": { 5 | "left-pad": "latest" 6 | } 7 | } -------------------------------------------------------------------------------- /test/fixtures/pnpmWorkspace/packages/b/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msr-test-b", 3 | "version": "0.0.0", 4 | "dependencies": { 5 | "msr-test-a": "*" 6 | }, 7 | "devDependencies": { 8 | "msr-test-d": "*", 9 | "left-pad": "latest" 10 | } 11 | } -------------------------------------------------------------------------------- /test/fixtures/pnpmWorkspace/packages/c/.releaserc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tagFormat": "multi-semantic-release-test-c@v${version}" 3 | } -------------------------------------------------------------------------------- /test/fixtures/pnpmWorkspace/packages/c/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msr-test-c", 3 | "version": "0.0.0", 4 | "devDependencies": { 5 | "msr-test-b": "*", 6 | "msr-test-d": "*" 7 | } 8 | } -------------------------------------------------------------------------------- /test/fixtures/pnpmWorkspace/packages/d/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msr-test-d", 3 | "version": "0.0.0" 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/pnpmWorkspace/pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'packages/*' 3 | -------------------------------------------------------------------------------- /test/fixtures/pnpmWorkspaceIgnore/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msr-test-pnpm", 3 | "author": "Dave Houlbrooke =8.3" 9 | }, 10 | "release": { 11 | "plugins": [ 12 | "@semantic-release/commit-analyzer", 13 | "@semantic-release/release-notes-generator" 14 | ], 15 | "noCi": true 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/fixtures/pnpmWorkspaceIgnore/packages/a/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msr-test-a", 3 | "version": "0.0.0", 4 | "peerDependencies": { 5 | "left-pad": "latest" 6 | } 7 | } -------------------------------------------------------------------------------- /test/fixtures/pnpmWorkspaceIgnore/packages/b/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msr-test-b", 3 | "version": "0.0.0", 4 | "dependencies": { 5 | "msr-test-a": "*" 6 | }, 7 | "devDependencies": { 8 | "msr-test-d": "*", 9 | "left-pad": "latest" 10 | } 11 | } -------------------------------------------------------------------------------- /test/fixtures/pnpmWorkspaceIgnore/packages/c/.releaserc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tagFormat": "multi-semantic-release-test-c@v${version}" 3 | } -------------------------------------------------------------------------------- /test/fixtures/pnpmWorkspaceIgnore/packages/c/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msr-test-c", 3 | "version": "0.0.0", 4 | "devDependencies": { 5 | "msr-test-b": "*", 6 | "msr-test-d": "*" 7 | } 8 | } -------------------------------------------------------------------------------- /test/fixtures/pnpmWorkspaceIgnore/packages/d/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msr-test-d", 3 | "version": "0.0.0" 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/pnpmWorkspaceIgnore/pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'packages/*' 3 | - '!packages/d/**' 4 | -------------------------------------------------------------------------------- /test/fixtures/pnpmWorkspaceUndefined/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msr-test-pnpm", 3 | "author": "Dave Houlbrooke ", 4 | "version": "0.0.0-semantically-released", 5 | "private": true, 6 | "license": "0BSD", 7 | "engines": { 8 | "node": ">=8.3" 9 | }, 10 | "release": { 11 | "plugins": [ 12 | "@semantic-release/commit-analyzer", 13 | "@semantic-release/release-notes-generator" 14 | ], 15 | "noCi": true 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/fixtures/pnpmWorkspaceUndefined/packages/a/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msr-test-a", 3 | "version": "0.0.0", 4 | "peerDependencies": { 5 | "msr-test-c": "*", 6 | "left-pad": "latest" 7 | } 8 | } -------------------------------------------------------------------------------- /test/fixtures/pnpmWorkspaceUndefined/packages/b/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msr-test-b", 3 | "version": "0.0.0", 4 | "dependencies": { 5 | "msr-test-a": "*" 6 | }, 7 | "devDependencies": { 8 | "msr-test-c": "*", 9 | "left-pad": "latest" 10 | } 11 | } -------------------------------------------------------------------------------- /test/fixtures/pnpmWorkspaceUndefined/packages/c/.releaserc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tagFormat": "multi-semantic-release-test-c@v${version}" 3 | } -------------------------------------------------------------------------------- /test/fixtures/pnpmWorkspaceUndefined/packages/c/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msr-test-c", 3 | "version": "0.0.0", 4 | "devDependencies": { 5 | "msr-test-b": "*", 6 | "msr-test-d": "*" 7 | } 8 | } -------------------------------------------------------------------------------- /test/fixtures/pnpmWorkspaceUndefined/packages/d/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msr-test-d", 3 | "version": "0.0.0" 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/pnpmWorkspaceUndefined/pnpm-workspace.yaml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiwi/multi-semantic-release/9adc120091a85b23a735de84ce702aa9d6c92c06/test/fixtures/pnpmWorkspaceUndefined/pnpm-workspace.yaml -------------------------------------------------------------------------------- /test/fixtures/undefinedYarnWorkspaces/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msr-test-yarn-undefined", 3 | "author": "Dave Houlbrooke =8.3" 9 | } 10 | } -------------------------------------------------------------------------------- /test/fixtures/yarnWorkspaces/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msr-test-yarn", 3 | "author": "Dave Houlbrooke =8.3" 9 | }, 10 | "workspaces": [ 11 | "packages/*" 12 | ], 13 | "release": { 14 | "plugins": [ 15 | "@semantic-release/commit-analyzer", 16 | "@semantic-release/release-notes-generator" 17 | ], 18 | "noCi": true 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test/fixtures/yarnWorkspaces/packages/a/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msr-test-a", 3 | "version": "0.0.0", 4 | "peerDependencies": { 5 | "left-pad": "latest" 6 | } 7 | } -------------------------------------------------------------------------------- /test/fixtures/yarnWorkspaces/packages/b/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msr-test-b", 3 | "version": "0.0.0", 4 | "dependencies": { 5 | "msr-test-a": "*" 6 | }, 7 | "devDependencies": { 8 | "msr-test-d": "*", 9 | "left-pad": "latest" 10 | } 11 | } -------------------------------------------------------------------------------- /test/fixtures/yarnWorkspaces/packages/c/.releaserc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tagFormat": "multi-semantic-release-test-c@v${version}" 3 | } -------------------------------------------------------------------------------- /test/fixtures/yarnWorkspaces/packages/c/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msr-test-c", 3 | "version": "0.0.0", 4 | "devDependencies": { 5 | "msr-test-b": "*", 6 | "msr-test-d": "*" 7 | } 8 | } -------------------------------------------------------------------------------- /test/fixtures/yarnWorkspaces/packages/d/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msr-test-d", 3 | "version": "0.0.0" 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/yarnWorkspaces2Packages/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msr-test-yarn", 3 | "author": "Dave Houlbrooke =8.3" 9 | }, 10 | "workspaces": [ 11 | "packages/*" 12 | ], 13 | "release": { 14 | "plugins": [ 15 | "@semantic-release/commit-analyzer", 16 | "@semantic-release/release-notes-generator" 17 | ], 18 | "noCi": true 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test/fixtures/yarnWorkspaces2Packages/packages/c/.releaserc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tagFormat": "multi-semantic-release-test-c@v${version}" 3 | } -------------------------------------------------------------------------------- /test/fixtures/yarnWorkspaces2Packages/packages/c/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msr-test-c", 3 | "version": "0.0.0", 4 | "dependencies": { 5 | "msr-test-d": "*" 6 | } 7 | } -------------------------------------------------------------------------------- /test/fixtures/yarnWorkspaces2Packages/packages/d/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msr-test-d", 3 | "version": "0.0.0" 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/yarnWorkspacesAcyclic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msr-test-yarn", 3 | "author": "Dave Houlbrooke =8.3" 9 | }, 10 | "workspaces": [ 11 | "packages/*" 12 | ], 13 | "release": { 14 | "plugins": [ 15 | "@semantic-release/commit-analyzer", 16 | "@semantic-release/release-notes-generator" 17 | ], 18 | "noCi": true 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test/fixtures/yarnWorkspacesAcyclic/packages/a/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msr-test-a", 3 | "version": "0.0.0", 4 | "peerDependencies": { 5 | "msr-test-c": "*", 6 | "left-pad": "latest" 7 | } 8 | } -------------------------------------------------------------------------------- /test/fixtures/yarnWorkspacesAcyclic/packages/b/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msr-test-b", 3 | "version": "0.0.0", 4 | "dependencies": { 5 | "msr-test-a": "*" 6 | }, 7 | "devDependencies": { 8 | "msr-test-c": "*", 9 | "left-pad": "latest" 10 | } 11 | } -------------------------------------------------------------------------------- /test/fixtures/yarnWorkspacesAcyclic/packages/c/.releaserc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tagFormat": "multi-semantic-release-test-c@v${version}" 3 | } -------------------------------------------------------------------------------- /test/fixtures/yarnWorkspacesAcyclic/packages/c/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msr-test-c", 3 | "version": "0.0.0", 4 | "devDependencies": { 5 | "msr-test-d": "*" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/fixtures/yarnWorkspacesAcyclic/packages/d/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msr-test-d", 3 | "version": "0.0.0" 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/yarnWorkspacesConfig/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msr-test-yarn", 3 | "author": "Dave Houlbrooke =8.3" 9 | }, 10 | "workspaces": { 11 | "packages": ["packages/*"] 12 | }, 13 | "release": { 14 | "plugins": [ 15 | "@semantic-release/commit-analyzer", 16 | "@semantic-release/release-notes-generator" 17 | ], 18 | "noCi": true 19 | }, 20 | "multi-release": { 21 | "debug": true, 22 | "ignorePackages": ["!packages/d/**"], 23 | "deps": { 24 | "bump": "inherit" 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /test/fixtures/yarnWorkspacesConfig/packages/a/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msr-test-a", 3 | "version": "0.0.0", 4 | "peerDependencies": { 5 | "msr-test-c": "*", 6 | "left-pad": "latest" 7 | } 8 | } -------------------------------------------------------------------------------- /test/fixtures/yarnWorkspacesConfig/packages/b/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msr-test-b", 3 | "version": "0.0.0", 4 | "dependencies": { 5 | "msr-test-a": "*" 6 | }, 7 | "devDependencies": { 8 | "msr-test-c": "*", 9 | "left-pad": "latest" 10 | } 11 | } -------------------------------------------------------------------------------- /test/fixtures/yarnWorkspacesConfig/packages/c/.releaserc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tagFormat": "multi-semantic-release-test-c@v${version}" 3 | } -------------------------------------------------------------------------------- /test/fixtures/yarnWorkspacesConfig/packages/c/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msr-test-c", 3 | "version": "0.0.0", 4 | "devDependencies": { 5 | "msr-test-b": "*", 6 | "msr-test-d": "*" 7 | } 8 | } -------------------------------------------------------------------------------- /test/fixtures/yarnWorkspacesConfig/packages/d/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msr-test-d", 3 | "version": "0.0.0" 4 | } -------------------------------------------------------------------------------- /test/fixtures/yarnWorkspacesConfigExtends/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | deps: { 3 | bump: "satisfy", 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /test/fixtures/yarnWorkspacesConfigExtends/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msr-test-yarn", 3 | "author": "Dave Houlbrooke =8.3" 9 | }, 10 | "workspaces": { 11 | "packages": ["packages/*"] 12 | }, 13 | "release": { 14 | "plugins": [ 15 | "@semantic-release/commit-analyzer", 16 | "@semantic-release/release-notes-generator" 17 | ], 18 | "noCi": true 19 | }, 20 | "multi-release": { 21 | "debug": true, 22 | "ignorePackages": ["!packages/d/**"], 23 | "deps": { 24 | "bump": "inherit" 25 | }, 26 | "extends": ["./config.js"] 27 | } 28 | } -------------------------------------------------------------------------------- /test/fixtures/yarnWorkspacesConfigExtends/packages/a/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msr-test-a", 3 | "version": "0.0.0", 4 | "peerDependencies": { 5 | "msr-test-c": "*", 6 | "left-pad": "latest" 7 | } 8 | } -------------------------------------------------------------------------------- /test/fixtures/yarnWorkspacesConfigExtends/packages/b/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msr-test-b", 3 | "version": "0.0.0", 4 | "dependencies": { 5 | "msr-test-a": "*" 6 | }, 7 | "devDependencies": { 8 | "msr-test-c": "*", 9 | "left-pad": "latest" 10 | } 11 | } -------------------------------------------------------------------------------- /test/fixtures/yarnWorkspacesConfigExtends/packages/c/.releaserc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tagFormat": "multi-semantic-release-test-c@v${version}" 3 | } -------------------------------------------------------------------------------- /test/fixtures/yarnWorkspacesConfigExtends/packages/c/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msr-test-c", 3 | "version": "0.0.0", 4 | "devDependencies": { 5 | "msr-test-b": "*", 6 | "msr-test-d": "*" 7 | } 8 | } -------------------------------------------------------------------------------- /test/fixtures/yarnWorkspacesConfigExtends/packages/d/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msr-test-d", 3 | "version": "0.0.0" 4 | } -------------------------------------------------------------------------------- /test/fixtures/yarnWorkspacesIgnore/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msr-test-yarn", 3 | "author": "Dave Houlbrooke =8.3" 9 | }, 10 | "workspaces": [ 11 | "packages/*", 12 | "!packages/d/**" 13 | ], 14 | "release": { 15 | "plugins": [ 16 | "@semantic-release/commit-analyzer", 17 | "@semantic-release/release-notes-generator" 18 | ], 19 | "noCi": true 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /test/fixtures/yarnWorkspacesIgnore/packages/a/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msr-test-a", 3 | "version": "0.0.0", 4 | "peerDependencies": { 5 | "left-pad": "latest" 6 | } 7 | } -------------------------------------------------------------------------------- /test/fixtures/yarnWorkspacesIgnore/packages/b/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msr-test-b", 3 | "version": "0.0.0", 4 | "dependencies": { 5 | "msr-test-a": "*" 6 | }, 7 | "devDependencies": { 8 | "msr-test-d": "*", 9 | "left-pad": "latest" 10 | } 11 | } -------------------------------------------------------------------------------- /test/fixtures/yarnWorkspacesIgnore/packages/c/.releaserc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tagFormat": "multi-semantic-release-test-c@v${version}" 3 | } -------------------------------------------------------------------------------- /test/fixtures/yarnWorkspacesIgnore/packages/c/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msr-test-c", 3 | "version": "0.0.0", 4 | "devDependencies": { 5 | "msr-test-b": "*", 6 | "msr-test-d": "*" 7 | } 8 | } -------------------------------------------------------------------------------- /test/fixtures/yarnWorkspacesIgnore/packages/d/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msr-test-d", 3 | "version": "0.0.0" 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/yarnWorkspacesIgnoreSplit/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msr-test-yarn", 3 | "author": "Dave Houlbrooke =8.3" 9 | }, 10 | "workspaces": [ 11 | "packages/a", 12 | "!packages/b", 13 | "packages/c", 14 | "!packages/d" 15 | ], 16 | "release": { 17 | "plugins": [ 18 | "@semantic-release/commit-analyzer", 19 | "@semantic-release/release-notes-generator" 20 | ], 21 | "noCi": true 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /test/fixtures/yarnWorkspacesIgnoreSplit/packages/a/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msr-test-a", 3 | "version": "0.0.0", 4 | "peerDependencies": { 5 | "msr-test-c": "*", 6 | "left-pad": "latest" 7 | } 8 | } -------------------------------------------------------------------------------- /test/fixtures/yarnWorkspacesIgnoreSplit/packages/b/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msr-test-b", 3 | "version": "0.0.0", 4 | "dependencies": { 5 | "msr-test-a": "*" 6 | }, 7 | "devDependencies": { 8 | "msr-test-c": "*", 9 | "left-pad": "latest" 10 | } 11 | } -------------------------------------------------------------------------------- /test/fixtures/yarnWorkspacesIgnoreSplit/packages/c/.releaserc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tagFormat": "multi-semantic-release-test-c@v${version}" 3 | } -------------------------------------------------------------------------------- /test/fixtures/yarnWorkspacesIgnoreSplit/packages/c/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msr-test-c", 3 | "version": "0.0.0", 4 | "devDependencies": { 5 | "msr-test-b": "*", 6 | "msr-test-d": "*" 7 | } 8 | } -------------------------------------------------------------------------------- /test/fixtures/yarnWorkspacesIgnoreSplit/packages/d/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msr-test-d", 3 | "version": "0.0.0" 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/yarnWorkspacesPackages/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msr-test-yarn", 3 | "author": "Dave Houlbrooke =8.3" 9 | }, 10 | "workspaces": { 11 | "packages": ["packages/*"] 12 | }, 13 | "release": { 14 | "plugins": [ 15 | "@semantic-release/commit-analyzer", 16 | "@semantic-release/release-notes-generator" 17 | ], 18 | "noCi": true 19 | } 20 | } -------------------------------------------------------------------------------- /test/fixtures/yarnWorkspacesPackages/packages/a/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msr-test-a", 3 | "version": "0.0.0", 4 | "peerDependencies": { 5 | "left-pad": "latest" 6 | } 7 | } -------------------------------------------------------------------------------- /test/fixtures/yarnWorkspacesPackages/packages/b/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msr-test-b", 3 | "version": "0.0.0", 4 | "dependencies": { 5 | "msr-test-a": "*" 6 | }, 7 | "devDependencies": { 8 | "msr-test-d": "*", 9 | "left-pad": "latest" 10 | } 11 | } -------------------------------------------------------------------------------- /test/fixtures/yarnWorkspacesPackages/packages/c/.releaserc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tagFormat": "multi-semantic-release-test-c@v${version}" 3 | } -------------------------------------------------------------------------------- /test/fixtures/yarnWorkspacesPackages/packages/c/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msr-test-c", 3 | "version": "0.0.0", 4 | "devDependencies": { 5 | "msr-test-b": "*", 6 | "msr-test-d": "*" 7 | } 8 | } -------------------------------------------------------------------------------- /test/fixtures/yarnWorkspacesPackages/packages/d/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msr-test-d", 3 | "version": "0.0.0" 4 | } 5 | -------------------------------------------------------------------------------- /test/helpers/file.js: -------------------------------------------------------------------------------- 1 | import { basename, join, resolve } from "path"; 2 | import { copyFileSync, existsSync, mkdirSync, lstatSync, readdirSync, readFileSync, writeFileSync } from "fs"; 3 | 4 | // Deep copy a directory. 5 | function copyDirectory(source, target) { 6 | // Checks. 7 | if (!isDirectory(source)) throw new Error("copyDirectory(): source must be an existant directory"); 8 | if (!isDirectory(target)) { 9 | // Try making it now (Tempy doesn't actually make the dir, just generates the path). 10 | mkdirSync(target); 11 | // If it doesn't exist after that there's an issue. 12 | if (!isDirectory(target)) throw new Error("copyDirectory(): target must be an existant directory"); 13 | } 14 | 15 | // Copy every file and dir in the dir. 16 | readdirSync(source).forEach((name) => { 17 | // Get full paths. 18 | const sourceFile = join(source, name); 19 | const targetFile = join(target, name); 20 | 21 | // Directory or file? 22 | if (isDirectory(sourceFile)) { 23 | // Possibly make directory. 24 | if (!existsSync(targetFile)) mkdirSync(targetFile); 25 | // Recursive copy directory. 26 | copyDirectory(sourceFile, targetFile); 27 | } else { 28 | // Copy file. 29 | copyFileSync(sourceFile, targetFile); 30 | } 31 | }); 32 | } 33 | 34 | // Is given path a directory? 35 | function isDirectory(path) { 36 | // String path that exists and is a directory. 37 | return typeof path === "string" && existsSync(path) && lstatSync(path).isDirectory(); 38 | } 39 | 40 | // Creates testing files on all specified folders. 41 | function createNewTestingFiles(folders, cwd) { 42 | folders.forEach((fld) => { 43 | writeFileSync(`${cwd}/${fld}test.txt`, fld); 44 | }); 45 | } 46 | 47 | function addPrereleaseToPackageRootConfig(rootDir, releaseBranch) { 48 | const packageUri = resolve(join(rootDir, "package.json")); 49 | const packageJson = JSON.parse(readFileSync(packageUri).toString()); 50 | 51 | packageJson.release.branches = ["master", { name: releaseBranch, prerelease: true }]; 52 | } 53 | 54 | // Exports. 55 | export { copyDirectory, isDirectory, createNewTestingFiles, addPrereleaseToPackageRootConfig }; 56 | -------------------------------------------------------------------------------- /test/helpers/git.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Lifted and tweaked from semantic-release because we follow how they test their internals. 3 | * https://github.com/semantic-release/semantic-release/blob/master/test/helpers/git-utils.js 4 | */ 5 | 6 | import { check } from "blork"; 7 | import { temporaryDirectory } from "tempy"; 8 | import { execaSync } from "execa"; 9 | import fileUrl from "file-url"; 10 | import gitLogParser from "git-log-parser"; 11 | import { array as getStreamArray } from "get-stream"; 12 | 13 | /** 14 | * @typedef {Object} Commit 15 | * @property {string} branch The commit branch. 16 | * @property {string} hash The commit hash. 17 | * @property {string} message The commit message. 18 | */ 19 | 20 | // Init. 21 | 22 | /** 23 | * Create a Git repository. 24 | * _Created in a temp folder._ 25 | * 26 | * @param {string} branch="master" The branch to initialize the repository to. 27 | * @return {Promise} Promise that resolves to string pointing to the CWD for the created Git repository. 28 | */ 29 | function gitInit(branch = "master") { 30 | // Check params. 31 | check(branch, "branch: kebab"); 32 | 33 | // Init Git in a temp directory. 34 | const cwd = temporaryDirectory(); 35 | execaSync("git", ["init"], { cwd }); 36 | execaSync("git", ["checkout", "-b", branch], { cwd }); 37 | 38 | // Disable GPG signing for commits. 39 | gitConfig(cwd, "commit.gpgsign", false); 40 | gitUser(cwd); 41 | 42 | // Return directory. 43 | return cwd; 44 | } 45 | 46 | /** 47 | * Create a remote Git repository. 48 | * _Created in a temp folder._ 49 | * 50 | * @return {Promise} Promise that resolves to string URL of the of the remote origin. 51 | */ 52 | function gitInitRemote() { 53 | // Init bare Git repository in a temp directory. 54 | const cwd = temporaryDirectory(); 55 | execaSync("git", ["init", "--bare"], { cwd }); 56 | 57 | // Turn remote path into a file URL. 58 | const url = fileUrl(cwd); 59 | 60 | // Return URL for remote. 61 | return url; 62 | } 63 | 64 | /** 65 | * Create a remote Git repository and set it as the origin for a Git repository. 66 | * _Created in a temp folder._ 67 | * 68 | * @param {string} cwd The cwd to create and set the origin for. 69 | * @param {string} releaseBranch="null" Optional branch to be added in case of prerelease is activated for a branch. 70 | * @return {Promise} Promise that resolves to string URL of the of the remote origin. 71 | */ 72 | function gitInitOrigin(cwd, releaseBranch = null) { 73 | // Check params. 74 | check(cwd, "cwd: absolute"); 75 | 76 | // Turn remote path into a file URL. 77 | const url = gitInitRemote(); 78 | 79 | // Set origin on local repo. 80 | execaSync("git", ["remote", "add", "origin", url], { cwd }); 81 | 82 | // Set up a release branch. Return to master afterwards. 83 | if (releaseBranch) { 84 | execaSync("git", ["checkout", "-b", releaseBranch], { cwd }); 85 | execaSync("git", ["checkout", "master"], { cwd }); 86 | } 87 | 88 | execaSync("git", ["push", "--all", "origin"], { cwd }); 89 | 90 | // Return URL for remote. 91 | return url; 92 | } 93 | 94 | // Add. 95 | 96 | /** 97 | * Add files to staged commit in a Git repository. 98 | * 99 | * @param {string} cwd The cwd to create and set the origin for. 100 | * @param {string} file="." The file to add, defaulting to "." (all files). 101 | * @return {Promise} Promise that resolves when done. 102 | */ 103 | function gitAdd(cwd, file = ".") { 104 | // Check params. 105 | check(cwd, "cwd: absolute"); 106 | 107 | // Await command. 108 | execaSync("git", ["add", file], { cwd }); 109 | } 110 | 111 | // Commits. 112 | 113 | /** 114 | * Create commit on a Git repository. 115 | * _Allows empty commits without any files added._ 116 | * 117 | * @param {string} cwd The CWD of the Git repository. 118 | * @param {string} message Commit message. 119 | * @returns {Promise} Promise that resolves to the SHA for the commit. 120 | */ 121 | function gitCommit(cwd, message) { 122 | // Check params. 123 | check(cwd, "cwd: absolute"); 124 | check(message, "message: string+"); 125 | 126 | // Await the command. 127 | execaSync("git", ["commit", "-m", message, "--no-gpg-sign"], { cwd }); 128 | 129 | // Return HEAD SHA. 130 | return gitGetHead(cwd); 131 | } 132 | 133 | /** 134 | * `git add .` followed by `git commit` 135 | * _Allows empty commits without any files added._ 136 | * 137 | * @param {string} cwd The CWD of the Git repository. 138 | * @param {string} message Commit message. 139 | * @returns {Promise} Promise that resolves to the SHA for the commit. 140 | */ 141 | function gitCommitAll(cwd, message) { 142 | // Check params. 143 | check(cwd, "cwd: absolute"); 144 | check(message, "message: string+"); 145 | 146 | // Await command. 147 | gitAdd(cwd); 148 | 149 | // Await command and return the SHA hash. 150 | return gitCommit(cwd, message); 151 | } 152 | 153 | // Push. 154 | 155 | /** 156 | * Push to a remote Git repository. 157 | * 158 | * @param {string} cwd The CWD of the Git repository. 159 | * @param {string} remote The remote repository URL or name. 160 | * @param {string} branch The branch to push. 161 | * @returns {Promise} Promise that resolves when done. 162 | * @throws {Error} if the push failed. 163 | */ 164 | function gitPush(cwd, remote = "origin", branch = "master") { 165 | // Check params. 166 | check(cwd, "cwd: absolute"); 167 | check(remote, "remote: string"); 168 | check(branch, "branch: lower"); 169 | 170 | // Await command. 171 | execaSync("git", ["push", "--tags", remote, `HEAD:${branch}`], { cwd }); 172 | } 173 | 174 | /** 175 | * Sets git user data. 176 | * 177 | * @param {string} cwd The CWD of the Git repository. 178 | * @param {string} name Committer name. 179 | * @param {string} email Committer email. 180 | * @returns {void} Return void. 181 | */ 182 | function gitUser(cwd, name = "Foo Bar", email = "email@foo.bar") { 183 | execaSync("git", ["config", "--local", "user.email", email], { cwd }); 184 | execaSync("git", ["config", "--local", "user.name", name], { cwd }); 185 | } 186 | 187 | // Branches. 188 | 189 | /** 190 | * Create a branch in a local Git repository. 191 | * 192 | * @param {string} cwd The CWD of the Git repository. 193 | * @param {string} branch Branch name to create. 194 | * @returns {Promise} Promise that resolves when done. 195 | */ 196 | function gitBranch(cwd, branch) { 197 | // Check params. 198 | check(cwd, "cwd: absolute"); 199 | check(branch, "branch: lower"); 200 | 201 | // Await command. 202 | execaSync("git", ["branch", branch], { cwd }); 203 | } 204 | 205 | /** 206 | * Checkout a branch in a local Git repository. 207 | * 208 | * @param {string} cwd The CWD of the Git repository. 209 | * @param {string} branch Branch name to checkout. 210 | * @returns {Promise} Promise that resolves when done. 211 | */ 212 | function gitCheckout(cwd, branch) { 213 | // Check params. 214 | check(cwd, "cwd: absolute"); 215 | check(branch, "branch: lower"); 216 | 217 | // Await command. 218 | execaSync("git", ["checkout", branch], { cwd }); 219 | } 220 | 221 | // Hashes. 222 | 223 | /** 224 | * Get the current HEAD SHA in a local Git repository. 225 | * 226 | * @param {string} cwd The CWD of the Git repository. 227 | * @return {Promise} Promise that resolves to the SHA of the head commit. 228 | */ 229 | function gitGetHead(cwd) { 230 | // Check params. 231 | check(cwd, "cwd: absolute"); 232 | 233 | // Await command and return HEAD SHA. 234 | return execaSync("git", ["rev-parse", "HEAD"], { cwd }).stdout; 235 | } 236 | 237 | // Tags. 238 | 239 | /** 240 | * Create a tag on the HEAD commit in a local Git repository. 241 | * 242 | * @param {string} cwd The CWD of the Git repository. 243 | * @param {string} tagName The tag name to create. 244 | * @param {string} hash=false SHA for the commit on which to create the tag. If falsy the tag is created on the latest commit. 245 | * @returns {Promise} Promise that resolves when done. 246 | */ 247 | function gitTag(cwd, tagName, hash = undefined) { 248 | // Check params. 249 | check(cwd, "cwd: absolute"); 250 | check(tagName, "tagName: string+"); 251 | check(hash, "hash: alphanumeric{40}?"); 252 | 253 | // Run command. 254 | execaSync("git", hash ? ["tag", "-f", tagName, hash] : ["tag", tagName], { cwd }); 255 | } 256 | 257 | /** 258 | * Get the tag associated with a commit SHA. 259 | * 260 | * @param {string} cwd The CWD of the Git repository. 261 | * @param {string} hash The commit SHA for which to retrieve the associated tag. 262 | * @return {Promise} The tag associated with the SHA in parameter or `null`. 263 | */ 264 | function gitGetTags(cwd, hash) { 265 | // Check params. 266 | check(cwd, "cwd: absolute"); 267 | check(hash, "hash: alphanumeric{40}"); 268 | 269 | // Run command. 270 | return execaSync("git", ["describe", "--tags", "--exact-match", hash], { cwd }).stdout; 271 | } 272 | 273 | /** 274 | * Get the first commit SHA tagged `tagName` in a local Git repository. 275 | * 276 | * @param {string} cwd The CWD of the Git repository. 277 | * @param {string} tagName Tag name for which to retrieve the commit sha. 278 | * @return {Promise} Promise that resolves to the SHA of the first commit associated with `tagName`. 279 | */ 280 | function gitGetTagHash(cwd, tagName) { 281 | // Check params. 282 | check(cwd, "cwd: absolute"); 283 | check(tagName, "tagName: string+"); 284 | 285 | // Run command. 286 | return execaSync("git", ["rev-list", "-1", tagName], { cwd }).stdout; 287 | } 288 | 289 | // Configs. 290 | 291 | /** 292 | * Add a Git config setting. 293 | * 294 | * @param {string} cwd The CWD of the Git repository. 295 | * @param {string} name Config name. 296 | * @param {any} value Config value. 297 | * @returns {Promise} Promise that resolves when done. 298 | */ 299 | function gitConfig(cwd, name, value) { 300 | // Check params. 301 | check(cwd, "cwd: absolute"); 302 | check(name, "name: string+"); 303 | 304 | // Run command. 305 | execaSync("git", ["config", "--add", name, value], { cwd }); 306 | } 307 | 308 | /** 309 | * Get a Git config setting. 310 | * 311 | * @param {string} cwd The CWD of the Git repository. 312 | * @param {string} name Config name. 313 | * @returns {Promise} Promise that resolves when done. 314 | */ 315 | function gitGetConfig(cwd, name) { 316 | // Check params. 317 | check(cwd, "cwd: absolute"); 318 | check(name, "name: string+"); 319 | 320 | // Run command. 321 | execaSync("git", ["config", name], { cwd }).stdout; 322 | } 323 | 324 | /** 325 | * Get the commit message log of given commit SHA or branch name. 326 | * 327 | * @param {string} cwd The CWD of the Git repository. 328 | * @param {integer} number Limit the number of commits to output. 329 | * @param {string} hash The commit SHA or branch name. 330 | * @return {Promise} Promise that resolve to commit log message. 331 | */ 332 | function gitGetLog(cwd, number, hash) { 333 | check(cwd, "cwd: absolute"); 334 | check(number, "number: integer"); 335 | check(hash, "hash: string+"); 336 | 337 | // Run command. 338 | return execaSync("git", ["log", `-${number}`, hash], { cwd }).stdout; 339 | } 340 | // Exports. 341 | export { 342 | gitInit, 343 | gitInitRemote, 344 | gitInitOrigin, 345 | gitAdd, 346 | gitCommit, 347 | gitCommitAll, 348 | gitPush, 349 | gitCheckout, 350 | gitGetHead, 351 | gitGetTags, 352 | gitTag, 353 | gitGetTagHash, 354 | gitConfig, 355 | gitGetConfig, 356 | gitGetLog, 357 | }; 358 | -------------------------------------------------------------------------------- /test/lib/cleanPath.test.js: -------------------------------------------------------------------------------- 1 | import cleanPath from "../../lib/cleanPath.js"; 2 | 3 | // Tests. 4 | describe("cleanPath()", () => { 5 | test("Relative without CWD", () => { 6 | expect(cleanPath("aaa")).toBe(`${process.cwd()}/aaa`); 7 | expect(cleanPath("aaa/")).toBe(`${process.cwd()}/aaa`); 8 | }); 9 | test("Relative with CWD", () => { 10 | expect(cleanPath("ccc", "/a/b/")).toBe(`/a/b/ccc`); 11 | expect(cleanPath("ccc", "/a/b")).toBe(`/a/b/ccc`); 12 | }); 13 | test("Absolute without CWD", () => { 14 | expect(cleanPath("/aaa")).toBe(`/aaa`); 15 | expect(cleanPath("/aaa/")).toBe(`/aaa`); 16 | expect(cleanPath("/a/b/c")).toBe(`/a/b/c`); 17 | expect(cleanPath("/a/b/c/")).toBe(`/a/b/c`); 18 | }); 19 | test("Absolute with CWD", () => { 20 | expect(cleanPath("/aaa", "/x/y/z")).toBe(`/aaa`); 21 | expect(cleanPath("/aaa/", "/x/y/z")).toBe(`/aaa`); 22 | expect(cleanPath("/a/b/c", "/x/y/z")).toBe(`/a/b/c`); 23 | expect(cleanPath("/a/b/c/", "/x/y/z")).toBe(`/a/b/c`); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /test/lib/getCommitsFiltered.test.js: -------------------------------------------------------------------------------- 1 | import { isAbsolute, join } from "path"; 2 | import { temporaryDirectory } from "tempy"; 3 | import { writeFileSync, mkdirSync } from "fs"; 4 | import getCommitsFiltered from "../../lib/getCommitsFiltered.js"; 5 | import { gitInit, gitCommitAll } from "../helpers/git.js"; 6 | 7 | // Tests. 8 | describe("getCommitsFiltered()", () => { 9 | test("Works correctly (no lastHead)", async () => { 10 | // Create Git repo with copy of Yarn workspaces fixture. 11 | const cwd = await gitInit(); 12 | writeFileSync(`${cwd}/AAA.txt`, "AAA"); 13 | const sha1 = await gitCommitAll(cwd, "Commit 1"); 14 | mkdirSync(`${cwd}/bbb`); 15 | writeFileSync(`${cwd}/bbb/BBB.txt`, "BBB"); 16 | const sha2 = await gitCommitAll(cwd, "Commit 2"); 17 | mkdirSync(`${cwd}/ccc`); 18 | writeFileSync(`${cwd}/ccc/CCC.txt`, "CCC"); 19 | const sha3 = await gitCommitAll(cwd, "Commit 3"); 20 | 21 | // Filter a single directory of the repo. 22 | const commits = await getCommitsFiltered(cwd, "bbb/"); 23 | expect(commits.length).toBe(1); 24 | expect(commits[0].hash).toBe(sha2); 25 | expect(commits[0].subject).toBe("Commit 2"); 26 | }); 27 | test("Works correctly (with lastHead)", async () => { 28 | // Create Git repo with copy of Yarn workspaces fixture. 29 | const cwd = await gitInit(); 30 | writeFileSync(`${cwd}/AAA.txt`, "AAA"); 31 | const sha1 = await gitCommitAll(cwd, "Commit 1"); 32 | mkdirSync(`${cwd}/bbb`); 33 | writeFileSync(`${cwd}/bbb/BBB.txt`, "BBB"); 34 | const sha2 = await gitCommitAll(cwd, "Commit 2"); 35 | mkdirSync(`${cwd}/ccc`); 36 | writeFileSync(`${cwd}/ccc/CCC.txt`, "CCC"); 37 | const sha3 = await gitCommitAll(cwd, "Commit 3"); 38 | 39 | // Filter a single directory of the repo since sha3 40 | const commits = await getCommitsFiltered(cwd, "bbb/", sha3); 41 | expect(commits.length).toBe(0); 42 | }); 43 | test("Works correctly (initial commit)", async () => { 44 | // Create Git repo with copy of Yarn workspaces fixture. 45 | const cwd = await gitInit(); 46 | mkdirSync(`${cwd}/bbb`); 47 | mkdirSync(`${cwd}/ccc`); 48 | writeFileSync(`${cwd}/AAA.txt`, "AAA"); 49 | writeFileSync(`${cwd}/bbb/BBB.txt`, "BBB"); 50 | writeFileSync(`${cwd}/ccc/CCC.txt`, "CCC"); 51 | const sha = await gitCommitAll(cwd, "Initial commit"); 52 | 53 | // Filter a single directory of the repo. 54 | const commits = await getCommitsFiltered(cwd, "bbb/"); 55 | expect(commits.length).toBe(1); 56 | expect(commits[0].hash).toBe(sha); 57 | }); 58 | test("TypeError if cwd is not absolute path to directory", async () => { 59 | await expect(getCommitsFiltered(123, ".")).rejects.toBeInstanceOf(TypeError); 60 | await expect(getCommitsFiltered(123, ".")).rejects.toMatchObject({ 61 | message: expect.stringMatching("cwd: Must be directory that exists in the filesystem"), 62 | }); 63 | await expect(getCommitsFiltered("aaa", ".")).rejects.toBeInstanceOf(TypeError); 64 | await expect(getCommitsFiltered("aaa", ".")).rejects.toMatchObject({ 65 | message: expect.stringMatching("cwd: Must be directory that exists in the filesystem"), 66 | }); 67 | const cwd = temporaryDirectory(); 68 | await expect(getCommitsFiltered(`${cwd}/abc`, ".")).rejects.toBeInstanceOf(TypeError); 69 | await expect(getCommitsFiltered(`${cwd}/abc`, ".")).rejects.toMatchObject({ 70 | message: expect.stringMatching("cwd: Must be directory that exists in the filesystem"), 71 | }); 72 | }); 73 | test("TypeError if dir is not path to directory", async () => { 74 | const cwd = temporaryDirectory(); 75 | await expect(getCommitsFiltered(cwd, 123)).rejects.toBeInstanceOf(TypeError); 76 | await expect(getCommitsFiltered(cwd, 123)).rejects.toMatchObject({ 77 | message: expect.stringMatching("dir: Must be valid path"), 78 | }); 79 | await expect(getCommitsFiltered(cwd, "abc")).rejects.toBeInstanceOf(TypeError); 80 | await expect(getCommitsFiltered(cwd, "abc")).rejects.toMatchObject({ 81 | message: expect.stringMatching("dir: Must be directory that exists in the filesystem"), 82 | }); 83 | await expect(getCommitsFiltered(cwd, `${cwd}/abc`)).rejects.toBeInstanceOf(TypeError); 84 | await expect(getCommitsFiltered(cwd, `${cwd}/abc`)).rejects.toMatchObject({ 85 | message: expect.stringMatching("dir: Must be directory that exists in the filesystem"), 86 | }); 87 | }); 88 | test("TypeError if dir is equal to cwd", async () => { 89 | const cwd = temporaryDirectory(); 90 | await expect(getCommitsFiltered(cwd, cwd)).rejects.toBeInstanceOf(TypeError); 91 | await expect(getCommitsFiltered(cwd, cwd)).rejects.toMatchObject({ 92 | message: expect.stringMatching("dir: Must not be equal to cwd"), 93 | }); 94 | await expect(getCommitsFiltered(cwd, ".")).rejects.toBeInstanceOf(TypeError); 95 | await expect(getCommitsFiltered(cwd, ".")).rejects.toMatchObject({ 96 | message: expect.stringMatching("dir: Must not be equal to cwd"), 97 | }); 98 | }); 99 | test("TypeError if dir is not inside cwd", async () => { 100 | const cwd = temporaryDirectory(); 101 | const dir = temporaryDirectory(); 102 | await expect(getCommitsFiltered(cwd, dir)).rejects.toBeInstanceOf(TypeError); 103 | await expect(getCommitsFiltered(cwd, dir)).rejects.toMatchObject({ 104 | message: expect.stringMatching("dir: Must be inside cwd"), 105 | }); 106 | await expect(getCommitsFiltered(cwd, "..")).rejects.toBeInstanceOf(TypeError); 107 | await expect(getCommitsFiltered(cwd, "..")).rejects.toMatchObject({ 108 | message: expect.stringMatching("dir: Must be inside cwd"), 109 | }); 110 | }); 111 | test("TypeError if lastHead is not 40char alphanumeric Git SHA hash", async () => { 112 | const cwd = temporaryDirectory(); 113 | mkdirSync(join(cwd, "dir")); 114 | await expect(getCommitsFiltered(cwd, "dir", false)).rejects.toBeInstanceOf(TypeError); 115 | await expect(getCommitsFiltered(cwd, "dir", false)).rejects.toMatchObject({ 116 | message: expect.stringMatching("lastHead: Must be alphanumeric string with size 40 or empty"), 117 | }); 118 | await expect(getCommitsFiltered(cwd, "dir", 123)).rejects.toBeInstanceOf(TypeError); 119 | await expect(getCommitsFiltered(cwd, "dir", 123)).rejects.toMatchObject({ 120 | message: expect.stringMatching("lastHead: Must be alphanumeric string with size 40 or empty"), 121 | }); 122 | await expect(getCommitsFiltered(cwd, "dir", "nottherightlength")).rejects.toBeInstanceOf(TypeError); 123 | await expect(getCommitsFiltered(cwd, "dir", "nottherightlength")).rejects.toMatchObject({ 124 | message: expect.stringMatching("lastHead: Must be alphanumeric string with size 40 or empty"), 125 | }); 126 | }); 127 | }); 128 | -------------------------------------------------------------------------------- /test/lib/getConfigMultiSemrel.test.js: -------------------------------------------------------------------------------- 1 | import getConfig from "../../lib/getConfigMultiSemrel.js"; 2 | import { gitInit } from "../helpers/git.js"; 3 | import { copyDirectory } from "../helpers/file.js"; 4 | 5 | // Tests. 6 | describe("getConfig()", () => { 7 | test("Default options", async () => { 8 | const result = await getConfig(process.cwd(), {}); 9 | expect(result).toMatchObject({ 10 | sequentialInit: false, 11 | sequentialPrepare: true, 12 | firstParent: false, 13 | debug: false, 14 | ignorePrivate: true, 15 | ignorePackages: [], 16 | tagFormat: "${name}@${version}", 17 | dryRun: undefined, 18 | deps: { 19 | bump: "override", 20 | release: "patch", 21 | prefix: "", 22 | }, 23 | }); 24 | }); 25 | 26 | test("Only CLI flags and default options", async () => { 27 | const cliFlags = { 28 | debug: true, 29 | dryRun: false, 30 | ignorePackages: ["!packages/d/**"], 31 | deps: { 32 | bump: "inherit", 33 | }, 34 | }; 35 | const result = await getConfig(process.cwd(), cliFlags); 36 | expect(result).toMatchObject({ 37 | sequentialInit: false, 38 | sequentialPrepare: true, 39 | firstParent: false, 40 | debug: true, 41 | ignorePrivate: true, 42 | ignorePackages: ["!packages/d/**"], 43 | tagFormat: "${name}@${version}", 44 | dryRun: false, 45 | deps: { 46 | bump: "inherit", 47 | release: "patch", 48 | prefix: "", 49 | }, 50 | }); 51 | }); 52 | 53 | test("package.json config", async () => { 54 | const cwd = await gitInit(); 55 | copyDirectory(`test/fixtures/yarnWorkspacesConfig/`, cwd); 56 | const result = await getConfig(cwd, {}); 57 | expect(result).toMatchObject({ 58 | sequentialInit: false, 59 | sequentialPrepare: true, 60 | firstParent: false, 61 | debug: true, 62 | ignorePrivate: true, 63 | ignorePackages: ["!packages/d/**"], 64 | tagFormat: "${name}@${version}", 65 | dryRun: undefined, 66 | deps: { 67 | bump: "inherit", 68 | release: "patch", 69 | prefix: "", 70 | }, 71 | }); 72 | }); 73 | 74 | test("package.json config and CLI flags", async () => { 75 | const cwd = await gitInit(); 76 | const cliFlags = { 77 | debug: false, 78 | ignorePackages: ["!packages/c/**"], 79 | deps: { 80 | release: "minor", 81 | }, 82 | }; 83 | copyDirectory(`test/fixtures/yarnWorkspacesConfig/`, cwd); 84 | const result = await getConfig(cwd, cliFlags); 85 | expect(result).toMatchObject({ 86 | sequentialInit: false, 87 | sequentialPrepare: true, 88 | firstParent: false, 89 | debug: false, 90 | ignorePrivate: true, 91 | ignorePackages: ["!packages/d/**", "!packages/c/**"], 92 | tagFormat: "${name}@${version}", 93 | dryRun: undefined, 94 | deps: { 95 | bump: "inherit", 96 | release: "minor", 97 | prefix: "", 98 | }, 99 | }); 100 | }); 101 | 102 | test("package.json extends", async () => { 103 | const cwd = await gitInit(); 104 | copyDirectory(`test/fixtures/yarnWorkspacesConfigExtends/`, cwd); 105 | const result = await getConfig(cwd, {}); 106 | expect(result).toMatchObject({ 107 | sequentialInit: false, 108 | sequentialPrepare: true, 109 | firstParent: false, 110 | debug: true, 111 | ignorePrivate: true, 112 | ignorePackages: ["!packages/d/**"], 113 | tagFormat: "${name}@${version}", 114 | dryRun: undefined, 115 | deps: { 116 | bump: "satisfy", 117 | release: "patch", 118 | prefix: "", 119 | }, 120 | }); 121 | }); 122 | 123 | test("package.json extends and CLI flags", async () => { 124 | const cwd = await gitInit(); 125 | const cliFlags = { 126 | debug: false, 127 | ignorePackages: ["!packages/c/**"], 128 | deps: { 129 | release: "minor", 130 | }, 131 | }; 132 | copyDirectory(`test/fixtures/yarnWorkspacesConfigExtends/`, cwd); 133 | const result = await getConfig(cwd, cliFlags); 134 | expect(result).toMatchObject({ 135 | sequentialInit: false, 136 | sequentialPrepare: true, 137 | firstParent: false, 138 | debug: false, 139 | ignorePrivate: true, 140 | ignorePackages: ["!packages/d/**", "!packages/c/**"], 141 | tagFormat: "${name}@${version}", 142 | dryRun: undefined, 143 | deps: { 144 | bump: "satisfy", 145 | release: "minor", 146 | prefix: "", 147 | }, 148 | }); 149 | }); 150 | }); 151 | -------------------------------------------------------------------------------- /test/lib/getPackagePaths.test.js: -------------------------------------------------------------------------------- 1 | import { resolve } from "path"; 2 | import { dirname } from "node:path"; 3 | import { fileURLToPath } from "node:url"; 4 | import { topo } from "@semrel-extra/topo"; 5 | 6 | const __dirname = dirname(fileURLToPath(import.meta.url)); 7 | const getPackagePaths = async (cwd, ignore = []) => { 8 | const workspacesExtra = ignore.map((item) => `!${item}`); 9 | const result = await topo({ cwd, workspacesExtra }); 10 | return Object.values(result.packages) 11 | .map((pkg) => pkg.manifestPath) 12 | .sort(); 13 | }; 14 | 15 | // Tests. 16 | describe("getPackagePaths()", () => { 17 | test("yarn: Works correctly with workspaces", async () => { 18 | const resolved = resolve(`${__dirname}/../fixtures/yarnWorkspaces`); 19 | expect(await getPackagePaths(resolved)).toEqual([ 20 | `${resolved}/packages/a/package.json`, 21 | `${resolved}/packages/b/package.json`, 22 | `${resolved}/packages/c/package.json`, 23 | `${resolved}/packages/d/package.json`, 24 | ]); 25 | }); 26 | test("yarn: Should ignore some packages", async () => { 27 | const resolved = resolve(`${__dirname}/../fixtures/yarnWorkspacesIgnore`); 28 | expect(await getPackagePaths(resolved)).toEqual([ 29 | `${resolved}/packages/a/package.json`, 30 | `${resolved}/packages/b/package.json`, 31 | `${resolved}/packages/c/package.json`, 32 | ]); 33 | 34 | const resolvedSplit = resolve(`${__dirname}/../fixtures/yarnWorkspacesIgnoreSplit`); 35 | expect(await getPackagePaths(resolvedSplit)).toEqual([ 36 | `${resolvedSplit}/packages/a/package.json`, 37 | `${resolvedSplit}/packages/c/package.json`, 38 | ]); 39 | }); 40 | test("yarn: Should ignore some packages via CLI", async () => { 41 | const resolved = resolve(`${__dirname}/../fixtures/yarnWorkspacesIgnore`); 42 | expect(await getPackagePaths(resolved, ["packages/a/**", "packages/b/**"])).toEqual([ 43 | `${resolved}/packages/c/package.json`, 44 | ]); 45 | 46 | const resolvedSplit = resolve(`${__dirname}/../fixtures/yarnWorkspacesIgnoreSplit`); 47 | expect(await getPackagePaths(resolvedSplit, ["packages/b", "packages/d"])).toEqual([ 48 | `${resolvedSplit}/packages/a/package.json`, 49 | `${resolvedSplit}/packages/c/package.json`, 50 | ]); 51 | }); 52 | // test("yarn: Should throw when ignored packages from CLI and workspaces sets an empty workspace list to be processed", async () => { 53 | // const resolved = resolve(`${__dirname}/../fixtures/yarnWorkspacesIgnore`); 54 | // expect(() => await getPackagePaths(resolved, ["packages/a/**", "packages/b/**", "packages/c/**"])).toThrow(TypeError); 55 | // expect(() => await getPackagePaths(resolved, ["packages/a/**", "packages/b/**", "packages/c/**"])).toThrow( 56 | // "package.json: Project must contain one or more workspace-packages" 57 | // ); 58 | // }); 59 | // test("yarn: Error if no workspaces setting", async () => { 60 | // const resolved = resolve(`${__dirname}/../fixtures/emptyYarnWorkspaces`); 61 | // expect(() => await getPackagePaths(resolved)).toThrow(Error); 62 | // expect(() => await getPackagePaths(resolved)).toThrow("contain one or more workspace-packages"); 63 | // }); 64 | test("yarn: Works correctly with workspaces.packages", async () => { 65 | const resolved = resolve(`${__dirname}/../fixtures/yarnWorkspacesPackages`); 66 | expect(await getPackagePaths(resolved)).toEqual([ 67 | `${resolved}/packages/a/package.json`, 68 | `${resolved}/packages/b/package.json`, 69 | `${resolved}/packages/c/package.json`, 70 | `${resolved}/packages/d/package.json`, 71 | ]); 72 | }); 73 | test("pnpm: Works correctly with workspace", async () => { 74 | const resolved = resolve(`${__dirname}/../fixtures/pnpmWorkspace`); 75 | expect(await getPackagePaths(resolved)).toEqual([ 76 | `${resolved}/packages/a/package.json`, 77 | `${resolved}/packages/b/package.json`, 78 | `${resolved}/packages/c/package.json`, 79 | `${resolved}/packages/d/package.json`, 80 | ]); 81 | }); 82 | // test("pnpm: Error if no workspaces setting", async () => { 83 | // const resolved = resolve(`${__dirname}/../fixtures/pnpmWorkspaceUndefined`); 84 | // expect(() => await getPackagePaths(resolved)).toThrow(Error); 85 | // expect(() => await getPackagePaths(resolved)).toThrow("contain one or more workspace-packages"); 86 | // }); 87 | test("pnpm: Should ignore some packages", async () => { 88 | const resolved = resolve(`${__dirname}/../fixtures/pnpmWorkspaceIgnore`); 89 | expect(await getPackagePaths(resolved)).toEqual([ 90 | `${resolved}/packages/a/package.json`, 91 | `${resolved}/packages/b/package.json`, 92 | `${resolved}/packages/c/package.json`, 93 | ]); 94 | }); 95 | test("pnpm: Should ignore some packages via CLI", async () => { 96 | const resolved = resolve(`${__dirname}/../fixtures/pnpmWorkspaceIgnore`); 97 | expect(await getPackagePaths(resolved, ["packages/a/**", "packages/b/**"])).toEqual([ 98 | `${resolved}/packages/c/package.json`, 99 | ]); 100 | }); 101 | test("bolt: Works correctly with workspaces", async () => { 102 | const resolved = resolve(`${__dirname}/../fixtures/boltWorkspaces`); 103 | expect(await getPackagePaths(resolved)).toEqual([ 104 | `${resolved}/packages/a/package.json`, 105 | `${resolved}/packages/b/package.json`, 106 | `${resolved}/packages/c/package.json`, 107 | `${resolved}/packages/d/package.json`, 108 | ]); 109 | }); 110 | // test("bolt: Error if no workspaces setting", async () => { 111 | // const resolved = resolve(`${__dirname}/../fixtures/boltWorkspacesUndefined`); 112 | // expect(() => await getPackagePaths(resolved)).toThrow(Error); 113 | // expect(() => await getPackagePaths(resolved)).toThrow("contain one or more workspace-packages"); 114 | // }); 115 | test("bolt: Should ignore some packages", async () => { 116 | const resolved = resolve(`${__dirname}/../fixtures/boltWorkspacesIgnore`); 117 | expect(await getPackagePaths(resolved)).toEqual([ 118 | `${resolved}/packages/a/package.json`, 119 | `${resolved}/packages/b/package.json`, 120 | `${resolved}/packages/c/package.json`, 121 | ]); 122 | }); 123 | test("bolt: Should ignore some packages via CLI", async () => { 124 | const resolved = resolve(`${__dirname}/../fixtures/boltWorkspacesIgnore`); 125 | expect(await getPackagePaths(resolved, ["packages/a/**", "packages/b/**"])).toEqual([ 126 | `${resolved}/packages/c/package.json`, 127 | ]); 128 | }); 129 | }); 130 | -------------------------------------------------------------------------------- /test/lib/git.test.js: -------------------------------------------------------------------------------- 1 | import { temporaryDirectory } from "tempy"; 2 | import { WritableStreamBuffer } from "stream-buffers"; 3 | 4 | import { copyDirectory, createNewTestingFiles } from "../helpers/file.js"; 5 | import { gitInit, gitCommitAll, gitInitOrigin, gitPush } from "../helpers/git.js"; 6 | import { getTags } from "../../lib/git.js"; 7 | import multiSemanticRelease from "../../lib/multiSemanticRelease.js"; 8 | 9 | const env = {}; 10 | 11 | test("Fetch all tags on master after two package release", async () => { 12 | const packages = ["packages/c/", "packages/d/"]; 13 | 14 | // Create Git repo with copy of Yarn workspaces fixture. 15 | const cwd = gitInit("master", "release"); 16 | copyDirectory(`test/fixtures/yarnWorkspaces2Packages/`, cwd); 17 | const sha1 = gitCommitAll(cwd, "feat: Initial release"); 18 | gitInitOrigin(cwd, "release"); 19 | gitPush(cwd); 20 | 21 | const stdout = new WritableStreamBuffer(); 22 | const stderr = new WritableStreamBuffer(); 23 | 24 | // Call multiSemanticRelease() 25 | // Doesn't include plugins that actually publish. 26 | await multiSemanticRelease( 27 | packages.map((folder) => `${folder}package.json`), 28 | { 29 | branches: [{ name: "master" }, { name: "release" }], 30 | }, 31 | { cwd, stdout, stderr, env } 32 | ); 33 | 34 | const tags = getTags("master", { cwd }).sort(); 35 | expect(tags).toEqual(["msr-test-d@1.0.0", "msr-test-c@1.0.0"].sort()); 36 | }); 37 | 38 | test("Fetch only prerelease tags", async () => { 39 | const packages = ["packages/c/", "packages/d/"]; 40 | 41 | // Create Git repo with copy of Yarn workspaces fixture. 42 | const cwd = gitInit("master", "release"); 43 | copyDirectory(`test/fixtures/yarnWorkspaces2Packages/`, cwd); 44 | const sha1 = gitCommitAll(cwd, "feat: Initial release"); 45 | gitInitOrigin(cwd, "release"); 46 | gitPush(cwd); 47 | 48 | let stdout = new WritableStreamBuffer(); 49 | let stderr = new WritableStreamBuffer(); 50 | 51 | // Call multiSemanticRelease() 52 | // Doesn't include plugins that actually publish. 53 | await multiSemanticRelease( 54 | packages.map((folder) => `${folder}package.json`), 55 | { 56 | branches: [{ name: "master" }, { name: "release" }], 57 | }, 58 | { cwd, stdout, stderr, env } 59 | ); 60 | 61 | // Add new testing files for a new release. 62 | createNewTestingFiles(packages, cwd); 63 | const sha = gitCommitAll(cwd, "feat: New prerelease\n\nBREAKING CHANGE: bump to bigger value"); 64 | gitPush(cwd); 65 | 66 | // Capture output. 67 | stdout = new WritableStreamBuffer(); 68 | stderr = new WritableStreamBuffer(); 69 | 70 | // Call multiSemanticRelease() for a second release 71 | // Doesn't include plugins that actually publish. 72 | // Change the master branch from release to prerelease to test bumping. 73 | await multiSemanticRelease( 74 | packages.map((folder) => `${folder}package.json`), 75 | { 76 | branches: [{ name: "master", prerelease: "beta" }, { name: "release" }], 77 | }, 78 | { cwd, stdout, stderr, env } 79 | ); 80 | 81 | const tags = getTags("master", { cwd }, ["beta"]).sort(); 82 | expect(tags).toEqual(["msr-test-d@2.0.0-beta.1", "msr-test-c@2.0.0-beta.1"].sort()); 83 | }); 84 | 85 | test("Throws error if obtaining the tags fails", () => { 86 | const cwd = temporaryDirectory(); 87 | 88 | const t = () => { 89 | getTags("master", { cwd }); 90 | }; 91 | expect(t).toThrow(Error); 92 | }); 93 | -------------------------------------------------------------------------------- /test/lib/recognizeFormat.test.js: -------------------------------------------------------------------------------- 1 | import recognizeFormat from "../../lib/recognizeFormat.js"; 2 | 3 | // Tests. 4 | describe("recognizeFormat()", () => { 5 | describe("Indentation", () => { 6 | test("Normal indentation", () => 7 | expect( 8 | recognizeFormat(`{ 9 | "a": "b", 10 | "c": { 11 | "d": "e" 12 | } 13 | }`).indent 14 | ).toBe("\t")); 15 | test("No indentation", () => expect(recognizeFormat('{"a": "b"}').indent).toBe("")); 16 | }); 17 | 18 | describe("Trailing whitespace", () => { 19 | test("No trailing whitespace", () => expect(recognizeFormat('{"a": "b"}').trailingWhitespace).toBe("")); 20 | test("Newline", () => expect(recognizeFormat('{"a": "b"}\n').trailingWhitespace).toBe("\n")); 21 | test("Multiple newlines", () => expect(recognizeFormat('{"a": "b"}\n\n').trailingWhitespace).toBe("\n")); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /test/lib/resolveReleaseType.test.js: -------------------------------------------------------------------------------- 1 | import { resolveReleaseType } from "../../lib/updateDeps.js"; 2 | 3 | // Tests. 4 | describe("resolveReleaseType()", () => { 5 | test("Works correctly with no deps", () => { 6 | expect(resolveReleaseType({ localDeps: [] })).toBe(undefined); 7 | }); 8 | test("Works correctly with deps", () => { 9 | const pkg1 = { _nextType: "patch", localDeps: [] }; 10 | expect(resolveReleaseType(pkg1)).toBe("patch"); 11 | const pkg2 = { _nextType: undefined, localDeps: [] }; 12 | expect(resolveReleaseType(pkg2)).toBe(undefined); 13 | const pkg3 = { 14 | _nextType: undefined, 15 | localDeps: [ 16 | { _nextType: false, localDeps: [] }, 17 | { _nextType: false, localDeps: [] }, 18 | ], 19 | }; 20 | expect(resolveReleaseType(pkg3)).toBe(undefined); 21 | const pkg4 = { 22 | manifest: { dependencies: { a: "1.0.0" } }, 23 | _nextType: undefined, 24 | localDeps: [ 25 | { name: "a", _nextType: "patch", localDeps: [], _lastRelease: { version: "1.0.0" } }, 26 | { name: "b", _nextType: false, localDeps: [], _lastRelease: { version: "1.0.0" } }, 27 | ], 28 | }; 29 | expect(resolveReleaseType(pkg4)).toBe("patch"); 30 | const pkg5 = { 31 | _nextType: undefined, 32 | localDeps: [ 33 | { 34 | _nextType: false, 35 | localDeps: [ 36 | { _nextType: false, localDeps: [] }, 37 | { _nextType: false, localDeps: [] }, 38 | ], 39 | }, 40 | ], 41 | }; 42 | expect(resolveReleaseType(pkg5)).toBe(undefined); 43 | const pkg6 = { 44 | manifest: { dependencies: { a: "1.0.0" } }, 45 | _nextType: undefined, 46 | localDeps: [ 47 | { 48 | name: "a", 49 | _lastRelease: { version: "1.0.0" }, 50 | _nextType: false, 51 | manifest: { dependencies: { b: "1.0.0", c: "1.0.0", d: "1.0.0" } }, 52 | localDeps: [ 53 | { name: "b", _nextType: false, localDeps: [], _lastRelease: { version: "1.0.0" } }, 54 | { name: "c", _nextType: "patch", localDeps: [], _lastRelease: { version: "1.0.0" } }, 55 | { name: "d", _nextType: "major", localDeps: [], _lastRelease: { version: "1.0.0" } }, 56 | ], 57 | }, 58 | ], 59 | }; 60 | expect(resolveReleaseType(pkg6, "override", "inherit")).toBe("major"); 61 | }); 62 | test("No infinite loops", () => { 63 | const pkg1 = { _nextType: "patch", localDeps: [] }; 64 | pkg1.localDeps.push(pkg1); 65 | expect(resolveReleaseType(pkg1)).toBe("patch"); 66 | const pkg2 = { _nextType: undefined, localDeps: [] }; 67 | pkg2.localDeps.push(pkg2); 68 | expect(resolveReleaseType(pkg2)).toBe(undefined); 69 | const pkg3 = { 70 | _nextType: undefined, 71 | localDeps: [ 72 | { _nextType: false, localDeps: [] }, 73 | { _nextType: false, localDeps: [] }, 74 | ], 75 | }; 76 | pkg3.localDeps[0].localDeps.push(pkg3.localDeps[0]); 77 | expect(resolveReleaseType(pkg3)).toBe(undefined); 78 | const pkg4 = { 79 | manifest: { dependencies: { a: "1.0.0", b: "1.0.0" } }, 80 | _nextType: undefined, 81 | localDeps: [ 82 | { name: "a", _nextType: "patch", localDeps: [], _lastRelease: { version: "1.0.0" } }, 83 | { name: "b", _nextType: "major", localDeps: [], _lastRelease: { version: "1.0.0" } }, 84 | ], 85 | }; 86 | pkg4.localDeps[0].localDeps.push(pkg4.localDeps[0]); 87 | expect(resolveReleaseType(pkg4)).toBe("patch"); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /test/lib/updateDeps.test.js: -------------------------------------------------------------------------------- 1 | import { beforeAll, beforeEach, jest } from "@jest/globals"; 2 | jest.unstable_mockModule("../../lib/git.js", () => ({ 3 | getTags: jest.fn(), 4 | })); 5 | let resolveReleaseType, 6 | resolveNextVersion, 7 | getNextVersion, 8 | getNextPreVersion, 9 | getPreReleaseTag, 10 | getVersionFromTag, 11 | getTags; 12 | 13 | beforeAll(async () => { 14 | ({ getTags } = await import("../../lib/git.js")); 15 | ({ 16 | resolveReleaseType, 17 | resolveNextVersion, 18 | getNextVersion, 19 | getNextPreVersion, 20 | getPreReleaseTag, 21 | getVersionFromTag, 22 | } = await import("../../lib/updateDeps.js")); 23 | }); 24 | 25 | describe("resolveNextVersion()", () => { 26 | // prettier-ignore 27 | const cases = [ 28 | ["1.0.0", "1.0.1", undefined, "1.0.1"], 29 | ["1.0.0", "1.0.1", "override", "1.0.1"], 30 | 31 | ["*", "1.3.0", "satisfy", "*"], 32 | ["^1.0.0", "1.0.1", "satisfy", "^1.0.0"], 33 | ["^1.2.0", "1.3.0", "satisfy", "^1.2.0"], 34 | ["1.2.x", "1.2.2", "satisfy", "1.2.x"], 35 | 36 | ["~1.0.0", "1.1.0", "inherit", "~1.1.0"], 37 | ["1.2.x", "1.2.1", "inherit", "1.2.x"], 38 | ["1.2.x", "1.3.0", "inherit", "1.3.x"], 39 | ["^1.0.0", "2.0.0", "inherit", "^2.0.0"], 40 | ["*", "2.0.0", "inherit", "*"], 41 | ["~1.0", "2.0.0", "inherit", "~2.0"], 42 | ["~2.0", "2.1.0", "inherit", "~2.1"], 43 | ] 44 | 45 | cases.forEach(([currentVersion, nextVersion, strategy, resolvedVersion]) => { 46 | it(`${currentVersion}/${nextVersion}/${strategy} gives ${resolvedVersion}`, () => { 47 | expect(resolveNextVersion(currentVersion, nextVersion, strategy)).toBe(resolvedVersion); 48 | }); 49 | }); 50 | }); 51 | 52 | describe("resolveReleaseType()", () => { 53 | // prettier-ignore 54 | const cases = [ 55 | [ 56 | "returns own package's _nextType if exists", 57 | { 58 | _nextType: "patch", 59 | localDeps: [], 60 | }, 61 | undefined, 62 | undefined, 63 | "patch", 64 | ], 65 | [ 66 | "implements `inherit` strategy: returns the highest release type of any deps", 67 | { 68 | manifest: { dependencies: { a: "1.0.0" } }, 69 | _nextType: undefined, 70 | localDeps: [ 71 | { 72 | name: "a", 73 | manifest: { dependencies: { b: "1.0.0", c: "1.0.0", d: "1.0.0" } }, 74 | _lastRelease: { version: "1.0.0" }, 75 | _nextType: false, 76 | localDeps: [ 77 | { name: "b", _nextType: false, localDeps: [], _lastRelease: { version: "1.0.0" } }, 78 | { name: "c", _nextType: "patch", localDeps: [], _lastRelease: { version: "1.0.0" } }, 79 | { name: "d", _nextType: "major", localDeps: [], _lastRelease: { version: "1.0.0" } }, 80 | ], 81 | }, 82 | ], 83 | }, 84 | undefined, 85 | "inherit", 86 | "major" 87 | ], 88 | [ 89 | "implements `inherit` strategy: returns the highest release type of any deps even if the local package has changes", 90 | { 91 | manifest: { dependencies: { a: "1.0.0" } }, 92 | _nextType: "minor", 93 | localDeps: [ 94 | { 95 | name: "a", 96 | manifest: { dependencies: { b: "1.0.0", c: "1.0.0", d: "1.0.0" } }, 97 | _lastRelease: { version: "1.0.0" }, 98 | _nextType: false, 99 | localDeps: [ 100 | { name: "b", _nextType: false, localDeps: [], _lastRelease: { version: "1.0.0" } }, 101 | { name: "c", _nextType: "patch", localDeps: [], _lastRelease: { version: "1.0.0" } }, 102 | { name: "d", _nextType: "major", localDeps: [], _lastRelease: { version: "1.0.0" } }, 103 | ], 104 | }, 105 | ], 106 | }, 107 | undefined, 108 | "inherit", 109 | "major" 110 | ], 111 | [ 112 | "overrides dependent release type with custom value if defined", 113 | { 114 | manifest: { dependencies: { a: "1.0.0" } }, 115 | _nextType: undefined, 116 | localDeps: [ 117 | { 118 | name: "a", 119 | // _lastRelease: { version: "1.0.0" }, 120 | manifest: { dependencies: { b: "1.0.0", c: "1.0.0", d: "1.0.0" } }, 121 | _nextType: false, 122 | localDeps: [ 123 | { name: "b", _nextType: false, localDeps: [], _lastRelease: { version: "1.0.0" } }, 124 | { name: "c", _nextType: "minor", localDeps: [], _lastRelease: { version: "1.0.0" } }, 125 | { name: "d", _nextType: "patch", localDeps: [], _lastRelease: { version: "1.0.0" } }, 126 | ], 127 | }, 128 | ], 129 | }, 130 | undefined, 131 | "major", 132 | "major" 133 | ], 134 | [ 135 | "uses `patch` strategy as default (legacy flow)", 136 | { 137 | manifest: { dependencies: { a: "1.0.0" } }, 138 | _nextType: undefined, 139 | localDeps: [ 140 | { 141 | name: "a", 142 | _nextType: false, 143 | //_lastRelease: { version: "1.0.0" }, 144 | manifest: { dependencies: { b: "1.0.0", c: "1.0.0", d: "1.0.0" } }, 145 | localDeps: [ 146 | { name: "b", _nextType: false, localDeps: [], _lastRelease: { version: "1.0.0" } }, 147 | { name: "c", _nextType: "minor", localDeps: [], _lastRelease: { version: "1.0.0" } }, 148 | { name: "d", _nextType: "major", localDeps: [], _lastRelease: { version: "1.0.0" } }, 149 | ], 150 | }, 151 | ], 152 | }, 153 | undefined, 154 | undefined, 155 | "patch" 156 | ], 157 | [ 158 | "returns undefined if no _nextRelease found", 159 | { 160 | _nextType: undefined, 161 | localDeps: [ 162 | { 163 | _nextType: false, 164 | localDeps: [ 165 | { _nextType: false, localDeps: [] }, 166 | { 167 | _nextType: undefined, 168 | localDeps: [ 169 | { _nextType: undefined, localDeps: [] } 170 | ] 171 | }, 172 | ], 173 | }, 174 | ], 175 | }, 176 | undefined, 177 | undefined, 178 | undefined, 179 | ], 180 | ] 181 | 182 | cases.forEach(([name, pkg, bumpStrategy, releaseStrategy, result]) => { 183 | it(name, () => { 184 | expect(resolveReleaseType(pkg, bumpStrategy, releaseStrategy)).toBe(result); 185 | }); 186 | }); 187 | }); 188 | 189 | it("`override` + `prefix` injects carets to the manifest", () => { 190 | const pkgB = { name: "b", _nextType: false, localDeps: [], _lastRelease: { version: "1.0.0" } }; 191 | const pkgC = { name: "c", _nextType: "minor", localDeps: [], _lastRelease: { version: "1.0.0" } }; 192 | const pkgD = { name: "d", _nextType: "patch", localDeps: [], _lastRelease: { version: "1.0.0" } }; 193 | const pkgA = { 194 | name: "a", 195 | manifest: { dependencies: { b: "1.0.0", c: "1.0.0", d: "1.0.0" } }, 196 | _nextType: false, 197 | localDeps: [pkgB, pkgC, pkgD], 198 | }; 199 | const pkg = { 200 | name: "root", 201 | manifest: { dependencies: { a: "1.0.0" } }, 202 | _nextType: undefined, 203 | localDeps: [pkgA], 204 | }; 205 | 206 | const type = resolveReleaseType(pkg, "override", "patch", [], "^"); 207 | 208 | expect(type).toBe("patch"); 209 | expect(pkg._nextType).toBe("patch"); 210 | 211 | expect(pkg.manifest.dependencies.a).toBe("^1.0.0"); 212 | expect(pkgA.manifest.dependencies.b).toBe("^1.0.0"); 213 | expect(pkgA.manifest.dependencies.c).toBe("^1.1.0"); 214 | expect(pkgA.manifest.dependencies.d).toBe("^1.0.1"); 215 | }); 216 | 217 | describe("getNextVersion()", () => { 218 | // prettier-ignore 219 | const cases = [ 220 | [undefined, "patch", "1.0.0"], 221 | ["1.0.0", "patch", "1.0.1"], 222 | ["2.0.0", undefined, "2.0.0"], 223 | ["1.0.0-dev.1", "major", "1.0.0"], 224 | ["1.0.0-dev.1", undefined, "1.0.0-dev.1"], 225 | ["1.0.0-dev.1", "minor", "1.0.0"], 226 | ["1.0.0-dev.1", "patch", "1.0.0"], 227 | ] 228 | 229 | cases.forEach(([lastVersion, releaseType, nextVersion, preRelease]) => { 230 | it(`${lastVersion} and ${releaseType} gives ${nextVersion}`, () => { 231 | // prettier-ignore 232 | expect(getNextVersion({ 233 | _nextType: releaseType, 234 | _lastRelease: {version: lastVersion}, 235 | _preRelease: preRelease 236 | })).toBe(nextVersion); 237 | }); 238 | }); 239 | }); 240 | 241 | describe("getNextPreVersion()", () => { 242 | beforeEach(() => { 243 | jest.clearAllMocks(); 244 | }); 245 | // prettier-ignore 246 | const cases = [ 247 | [undefined, "patch", "rc", [], "1.0.0-rc.1"], 248 | [undefined, "patch", "rc", [], "1.0.0-rc.1"], 249 | [null, "patch", "rc", [], "1.0.0-rc.1"], 250 | [null, "patch", "rc", [], "1.0.0-rc.1"], 251 | ["1.0.0-rc.0", "minor", "dev", [], "1.0.0-dev.1"], 252 | ["1.0.0-dev.0", "major", "dev", [], "1.0.0-dev.1"], 253 | ["1.0.0-dev.0", "major", "dev", ["testing-package@1.0.0-dev.1"], "1.0.0-dev.2"], 254 | ["1.0.0-dev.0", "major", "dev", ["testing-package@1.0.0-dev.1", "1.0.1-dev.0"], "1.0.1-dev.1"], 255 | ["11.0.0", "major", "beta", [], "12.0.0-beta.1"], 256 | ["1.0.0", "minor", "beta", [], "1.1.0-beta.1"], 257 | ["1.0.0", "patch", "beta", [], "1.0.1-beta.1"], 258 | ] 259 | 260 | cases.forEach(([lastVersion, releaseType, preRelease, lastTags, nextVersion]) => { 261 | it(`${lastVersion} and ${releaseType} ${ 262 | lastTags.length ? "with existent tags " : "" 263 | }gives ${nextVersion}`, () => { 264 | // prettier-ignore 265 | expect(getNextPreVersion( 266 | { 267 | _nextType: releaseType, 268 | _lastRelease: {version: lastVersion}, 269 | _preRelease: preRelease, 270 | _branch: "master", 271 | name: "testing-package", 272 | pullTagsForPrerelease: true, 273 | }, 274 | lastTags, 275 | )).toBe(nextVersion); 276 | }); 277 | it(`${lastVersion} and ${releaseType} ${ 278 | lastTags.length ? "with looked up branch tags " : "" 279 | }gives ${nextVersion}`, () => { 280 | getTags.mockImplementation(() => { 281 | return lastTags; 282 | }); 283 | // prettier-ignore 284 | expect(getNextPreVersion( 285 | { 286 | _nextType: releaseType, 287 | _lastRelease: {version: lastVersion}, 288 | _preRelease: preRelease, 289 | _branch: "master", 290 | name: "testing-package", 291 | pullTagsForPrerelease: true, 292 | }, 293 | // No Tags array means we look them up 294 | )).toBe(nextVersion); 295 | expect(getTags).toHaveBeenCalledTimes(1); 296 | }); 297 | }); 298 | it("does not allow tags if pullTagsForPrerelease = false", () => { 299 | expect(() => 300 | getNextPreVersion( 301 | { 302 | _nextType: "patch", 303 | _lastRelease: { version: "1.0.0" }, 304 | _preRelease: "dev", 305 | _branch: "master", 306 | name: "testing-package", 307 | pullTagsForPrerelease: false, 308 | }, 309 | [] 310 | ) 311 | ).toThrowError("Supplied tags for NextPreVersion but the package does not use tags for next prerelease"); 312 | }); 313 | // Simulates us not using tags as criteria 314 | 315 | const noTagCases = [ 316 | // prerelease channels just bump up the pre-release 317 | ["1.0.0-rc.0", "minor", "rc", "1.0.0-rc.1"], 318 | ["1.0.0-dev.0", "major", "dev", "1.0.0-dev.1"], 319 | ["1.0.0-dev.0", "major", "dev", "1.0.0-dev.1"], 320 | ["1.0.1-dev.0", "major", "dev", "1.0.1-dev.1"], 321 | // main channels obey the release type 322 | ["11.0.0", "major", "beta", "12.0.0-beta.1"], 323 | ["1.0.0", "minor", "beta", "1.1.0-beta.1"], 324 | ["1.0.0", "patch", "beta", "1.0.1-beta.1"], 325 | ]; 326 | noTagCases.forEach(([lastVersion, releaseType, preRelease, nextVersion]) => { 327 | it(`${lastVersion} and ${releaseType} for channel ${preRelease} gives ${nextVersion} when pullTagsForPrerelease = false`, () => { 328 | // prettier-ignore 329 | expect(getNextPreVersion( 330 | { 331 | _nextType: releaseType, 332 | _lastRelease: {version: lastVersion}, 333 | _preRelease: preRelease, 334 | _branch: "master", 335 | name: "testing-package", 336 | pullTagsForPrerelease: false, 337 | }, 338 | )).toBe(nextVersion); 339 | }); 340 | }); 341 | }); 342 | 343 | describe("getPreReleaseTag()", () => { 344 | // prettier-ignore 345 | const cases = [ 346 | [undefined, null], 347 | [null, null], 348 | ["1.0.0-rc.0", "rc"], 349 | ["1.0.0-dev.0", "dev"], 350 | ["1.0.0-dev.2", "dev"], 351 | ["1.1.0-beta.0", "beta"], 352 | ["11.0.0", null], 353 | ["11.1.0", null], 354 | ["11.0.1", null], 355 | ] 356 | 357 | cases.forEach(([version, preReleaseTag]) => { 358 | it(`${version} gives ${preReleaseTag}`, () => { 359 | // prettier-ignore 360 | expect(getPreReleaseTag(version)).toBe(preReleaseTag); 361 | }); 362 | }); 363 | }); 364 | 365 | describe("getVersionFromTag()", () => { 366 | // prettier-ignore 367 | const cases = [ 368 | [{}, undefined, null], 369 | [{ name: undefined }, undefined, null], 370 | [{}, null, null], 371 | [{ name: null }, null, null], 372 | [{ name: undefined }, '1.0.0', '1.0.0'], 373 | [{ name: null }, '1.0.0', '1.0.0'], 374 | [{ name: 'abc' }, undefined, null], 375 | [{ name: 'abc' }, null, null], 376 | [{ name: 'abc' }, '1.0.0', '1.0.0'], 377 | [{ name: 'dev' }, '1.0.0-dev.1', '1.0.0-dev.1'], 378 | [{ name: 'app' }, 'app@1.0.0-dev.1', '1.0.0-dev.1'], 379 | [{ name: 'app' }, 'app@1.0.0-devapp@.1', null], 380 | [{ name: 'msr-test-a' }, 'msr-test-a@1.0.0-rc.1', '1.0.0-rc.1'], 381 | [{ name: 'msr.test.a' }, 'msr.test.a@1.0.0', '1.0.0'], 382 | [{ name: 'msr_test_a' }, 'msr_test_a@1.0.0', '1.0.0'], 383 | [{ name: 'msr@test@a' }, 'msr@test@a@1.0.0', '1.0.0'], 384 | [{ name: 'abc' }, 'a.b.c-rc.0', null], 385 | [{ name: 'abc' }, '1-rc.0', null], 386 | [{ name: 'abc' }, '1.0.x-rc.0', null], 387 | [{ name: 'abc' }, '1.x.0-rc.0', null], 388 | [{ name: 'abc' }, 'x.1.0-rc.0', null], 389 | ] 390 | 391 | cases.forEach(([pkg, tag, versionFromTag]) => { 392 | it(`${JSON.stringify(pkg)} pkg with tag ${tag} gives ${versionFromTag}`, () => { 393 | // prettier-ignore 394 | expect(getVersionFromTag(pkg, tag)).toBe(versionFromTag); 395 | }); 396 | }); 397 | }); 398 | -------------------------------------------------------------------------------- /test/lib/utils.test.js: -------------------------------------------------------------------------------- 1 | import { getHighestVersion, getLowestVersion, getLatestVersion, tagsToVersions } from "../../lib/utils.js"; 2 | 3 | describe("tagsToVersions()", () => { 4 | // prettier-ignore 5 | const cases = [ 6 | [[{version: "1.0.0"}, {version: "1.1.0"}, {version: "1.2.0"}], ["1.0.0", "1.1.0", "1.2.0"]], 7 | [[],[]], 8 | [undefined, []], 9 | [null, []], 10 | ] 11 | 12 | cases.forEach(([tags, versions]) => { 13 | it(`${tags} gives versions as ${versions}`, () => { 14 | expect(tagsToVersions(tags)).toStrictEqual(versions); 15 | }); 16 | }); 17 | }); 18 | 19 | describe("getHighestVersion()", () => { 20 | // prettier-ignore 21 | const cases = [ 22 | ["1.0.0", "2.0.0", "2.0.0"], 23 | ["1.1.1", "1.0.0", "1.1.1"], 24 | [null, "1.0.0", "1.0.0"], 25 | ["1.0.0", undefined, "1.0.0"], 26 | [undefined, undefined, undefined], 27 | ] 28 | 29 | cases.forEach(([version1, version2, high]) => { 30 | it(`${version1}/${version2} gives highest as ${high}`, () => { 31 | expect(getHighestVersion(version1, version2)).toBe(high); 32 | }); 33 | }); 34 | }); 35 | 36 | describe("getLowestVersion()", () => { 37 | // prettier-ignore 38 | const cases = [ 39 | ["1.0.0", "2.0.0", "1.0.0"], 40 | ["1.1.1", "1.0.0", "1.0.0"], 41 | [null, "1.0.0", "1.0.0"], 42 | ["1.0.0", undefined, "1.0.0"], 43 | [undefined, undefined, undefined], 44 | ] 45 | 46 | cases.forEach(([version1, version2, low]) => { 47 | it(`${version1}/${version2} gives lowest as ${low}`, () => { 48 | expect(getLowestVersion(version1, version2, 0)).toBe(low); 49 | }); 50 | }); 51 | }); 52 | 53 | describe("getLatestVersion()", () => { 54 | // prettier-ignore 55 | const cases = [ 56 | [["1.2.3-alpha.3", "1.2.0", "1.0.1", "1.0.0-alpha.1"], null, "1.2.0"], 57 | [["1.2.3-alpha.3", "1.2.3-alpha.2"], null, undefined], 58 | [["1.2.3-alpha.3", "1.2.0", "1.0.1", "1.0.0-alpha.1"], true, "1.2.3-alpha.3"], 59 | [["1.2.3-alpha.3", "1.2.3-alpha.2"], true, "1.2.3-alpha.3"], 60 | [[], {}, undefined] 61 | ] 62 | 63 | cases.forEach(([versions, withPrerelease, latest]) => { 64 | it(`${versions}/${withPrerelease} gives latest as ${latest}`, () => { 65 | expect(getLatestVersion(versions, withPrerelease)).toBe(latest); 66 | }); 67 | }); 68 | }); 69 | --------------------------------------------------------------------------------