├── .bmp.yml ├── .editorconfig ├── .github └── workflows │ ├── ci.yml │ └── publish.yml ├── .gitignore ├── .vscode └── settings.json ├── LICENSE.md ├── README.md ├── __snapshots__ ├── mod_test.ts.snap └── util_test.ts.snap ├── cli.ts ├── deno.json ├── mod.ts ├── mod_test.ts ├── testdata ├── basic │ ├── bar │ │ └── deno.json │ ├── baz │ │ └── deno.json │ ├── deno.json │ ├── foo │ │ └── deno.json │ ├── quux │ │ └── deno.json │ └── qux │ │ └── deno.jsonc └── std_mock │ ├── collections │ └── deno.json │ ├── console │ └── deno.json │ ├── crypto │ └── deno.json │ ├── deno.json │ ├── expect │ └── deno.json │ ├── flags │ └── deno.json │ ├── fmt │ └── deno.json │ ├── http │ └── deno.json │ ├── io │ └── deno.json │ ├── log │ └── deno.json │ ├── media_types │ └── deno.json │ ├── msgpack │ └── deno.json │ ├── path │ └── deno.json │ ├── semver │ └── deno.json │ ├── streams │ └── deno.json │ ├── toml │ └── deno.json │ └── webgpu │ └── deno.json ├── util.ts └── util_test.ts /.bmp.yml: -------------------------------------------------------------------------------- 1 | version: 0.1.22 2 | commit: "%.%.%" 3 | files: 4 | README.md: "@deno/bump-workspaces@%.%.%" 5 | deno.json: '"version": "%.%.%"' 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root=true 2 | [*] 3 | indent_style=space 4 | indent_size=2 5 | trim_trailing_whitespace=true 6 | insert_final_newline=true 7 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | on: 3 | push: 4 | branches: [main] 5 | pull_request: 6 | branches: [main] 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | timeout-minutes: 10 11 | steps: 12 | - uses: actions/checkout@v3 13 | - uses: denoland/setup-deno@v1 14 | - run: deno fmt --check 15 | - run: deno lint 16 | - name: Test 17 | run: | 18 | git fetch origin # some branches are necessary for testing 19 | deno task cov 20 | - uses: codecov/codecov-action@v4 21 | env: 22 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 23 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: publish 2 | on: 3 | release: 4 | types: [published] 5 | jobs: 6 | publish: 7 | runs-on: ubuntu-latest 8 | permissions: 9 | contents: read 10 | id-token: write 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: denoland/setup-deno@v1 14 | - run: deno publish 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "deno.enable": true, 3 | "[json][typescript]": { 4 | "editor.formatOnSave": true, 5 | "editor.defaultFormatter": "denoland.vscode-deno", 6 | "editor.insertSpaces": true, 7 | "editor.tabSize": 2 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright 2024 the Deno authors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @deno/bump-workspaces 2 | 3 | > A tool for upgrading Deno workspace packages using conventional commits 4 | 5 | [![ci](https://github.com/denoland/bump-workspaces/actions/workflows/ci.yml/badge.svg)](https://github.com/denoland/bump-workspaces/actions/workflows/ci.yml) 6 | [![codecov](https://codecov.io/gh/denoland/bump-workspaces/graph/badge.svg?token=KUT5Q1PJE6)](https://codecov.io/gh/denoland/bump-workspaces) 7 | 8 | This tool detects necessary version upgrades for workspaces packages using 9 | [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) and 10 | creates a PR. 11 | 12 | # Try it 13 | 14 | Run this command with `--dry-run` flag in your Deno workspace-enabled project 15 | and see what this command does: 16 | 17 | ```sh 18 | deno run -A jsr:@deno/bump-workspaces@0.1.22/cli --dry-run 19 | ``` 20 | 21 | # How it works 22 | 23 | The below steps describe what this command does: 24 | 25 | - Read `deno.json` at the current directory. Read "workspaces". Read `deno.json` 26 | of each workspace package. 27 | - Collect the git commit messages between the latest tag and the current branch. 28 | - Calculate the necessary updates for each package. (See the below table for 29 | what version upgrades are performed for each conventional commit tag.) 30 | - Create and print the release note. 31 | - Stop here if `--dry-run` specified, and continue if not. 32 | - Save necessary updates to each `deno.json`. 33 | - Create a new branch `release-YYYY-MM-DD` 34 | - Make git commit the version changes using `GIT_USER_NAME` and `GIT_USER_EMAIL` 35 | env vars. 36 | - Create a github pull request using `GITHUB_TOKEN` and `GITHUB_REPOSITORY` env 37 | vars. 38 | - That's all. 39 | 40 | Note: Don't worry if your commits don't completely follow conventional commits. 41 | You can still manually update the PR generated by this tool. The PR body 42 | includes the information about which commits are handled by this tool and which 43 | are not. 44 | 45 | # CI set up 46 | 47 | Set up the GitHub Actions yaml like the below, and trigger the workflow 48 | manually: 49 | 50 | ```yaml 51 | name: version_bump 52 | 53 | on: workflow_dispatch 54 | 55 | jobs: 56 | build: 57 | name: version bump 58 | runs-on: ubuntu-latest 59 | timeout-minutes: 15 60 | 61 | steps: 62 | - name: Clone repository 63 | uses: actions/checkout@v4 64 | 65 | - name: Set up Deno 66 | uses: denoland/setup-deno@v1 67 | 68 | - name: Run workspaces version bump 69 | run: | 70 | git fetch --unshallow origin 71 | deno run -A jsr:@deno/bump-workspaces@0.1.22/cli 72 | env: 73 | GITHUB_TOKEN: ${{ secrets.BOT_TOKEN }} 74 | ``` 75 | 76 | Example pull request: https://github.com/kt3k/deno_std/pull/34 77 | 78 | ## Commit titles 79 | 80 | This tool uses the commit titles as the input for detecting which modules and 81 | versions to update. The commit titles need to follow the following format: 82 | 83 | ``` 84 | (): 85 | ``` 86 | 87 | Some examples are: 88 | 89 | ``` 90 | fix(foo): fix a bug 91 | fix(baz,qux): fix a bug 92 | feat(bar): add a new feature 93 | chore(foo): clean up 94 | chore(bar): clean up 95 | BREAKING(quux): some breaking change 96 | ``` 97 | 98 | This example results in the following version updates: 99 | 100 | | module | version | 101 | | ------ | ------- | 102 | | foo | patch | 103 | | bar | minor | 104 | | baz | patch | 105 | | qux | patch | 106 | | quux | major | 107 | 108 | The tool automatically detects following commit tags: 109 | 110 | - BREAKING 111 | - feat 112 | - fix 113 | - perf 114 | - docs 115 | - deprecation 116 | - refactor 117 | - test 118 | - style 119 | - chore 120 | 121 | If a module has `BREAKING` commits, then `major` version will be updated. If a 122 | module has `feat` commits, `minor` version will be updated. Otherwise `patch` 123 | version will be updated. 124 | 125 | | tag | version | 126 | | ----------- | ------- | 127 | | BREAKING | major | 128 | | feat | minor | 129 | | fix | patch | 130 | | perf | patch | 131 | | docs | patch | 132 | | deprecation | patch | 133 | | refactor | patch | 134 | | test | patch | 135 | | style | patch | 136 | | chore | patch | 137 | 138 | ## Scope required tags 139 | 140 | The following tags require scope specified because they don't make sense without 141 | scopes. If these tags specified without scopes, they are raised as diagnostics 142 | in README. 143 | 144 | - BREAKING 145 | - feat 146 | - fix 147 | - perf 148 | - deprecation 149 | 150 | ## Wildcard scope 151 | 152 | You can use `*` for the scope. That commit affects all the packages in the 153 | workspace. For example: 154 | 155 | ``` 156 | refactor(*): clean up 157 | ``` 158 | 159 | The above commit causes `patch` upgrade to the all packages. 160 | 161 | ## Unstable updates 162 | 163 | You can mark the change only affects the unstable part of the package by using 164 | `scope/unstable` or `unstable/scope`. 165 | 166 | ``` 167 | feat(crypto/unstable): a new unstable feature 168 | BREAKING(crypto/unstable): breaking change to unstable feature 169 | ``` 170 | 171 | If this notation is used, the effect of the commit becomes `patch` no matter 172 | what commit type is used. 173 | 174 | # License 175 | 176 | MIT 177 | -------------------------------------------------------------------------------- /__snapshots__/mod_test.ts.snap: -------------------------------------------------------------------------------- 1 | export const snapshot = {}; 2 | 3 | snapshot[`bumpWorkspaces() 1`] = ` 4 | "### YYYY.MM.DD 5 | 6 | #### @scope/bar 2.3.5 (patch) 7 | 8 | - fix(foo,bar,baz,qux,quux): a fix 9 | - chore(bar): a chore 10 | 11 | #### @scope/baz 0.2.4 (patch) 12 | 13 | - feat(baz): add a feature 14 | - fix(foo,bar,baz,qux,quux): a fix 15 | 16 | #### @scope/foo 2.0.0 (major) 17 | 18 | - BREAKING(foo): a breaking change 19 | - deprecation(foo): a deprecation 20 | - fix(foo,bar,baz,qux,quux): a fix 21 | - docs(foo): add docs 22 | 23 | #### @scope/quux 0.1.0 (minor) 24 | 25 | - BREAKING(quux): a breaking change 26 | - fix(foo,bar,baz,qux,quux): a fix 27 | - style(qux,quux): style update 28 | - test(quux): add a test 29 | 30 | #### @scope/qux 0.3.5 (patch) 31 | 32 | - fix(foo,bar,baz,qux,quux): a fix 33 | - perf(qux): a perf improvement 34 | - style(qux,quux): style update 35 | - chore(qux): a chore 36 | " 37 | `; 38 | -------------------------------------------------------------------------------- /__snapshots__/util_test.ts.snap: -------------------------------------------------------------------------------- 1 | export const snapshot = {}; 2 | 3 | snapshot[`summarizeVersionBumpsByModule() 1`] = ` 4 | [ 5 | { 6 | commits: [ 7 | { 8 | body: "", 9 | hash: "0000000000000000000000000000000000000000", 10 | subject: "feat(collections): pass \`key\` to \`mapValues()\` transformer (#4127)", 11 | tag: "feat", 12 | }, 13 | ], 14 | module: "collections", 15 | version: "minor", 16 | }, 17 | { 18 | commits: [ 19 | { 20 | body: "", 21 | hash: "0000000000000000000000000000000000000000", 22 | subject: "refactor(console): rename \`_rle\` to \`_run_length.ts\` (#4212)", 23 | tag: "refactor", 24 | }, 25 | ], 26 | module: "console", 27 | version: "patch", 28 | }, 29 | { 30 | commits: [ 31 | { 32 | body: "", 33 | hash: "0000000000000000000000000000000000000000", 34 | subject: "chore(crypto): upgrade to \`rust@1.75.0\` and \`wasmbuild@0.15.5\` (#4193)", 35 | tag: "chore", 36 | }, 37 | ], 38 | module: "crypto", 39 | version: "patch", 40 | }, 41 | { 42 | commits: [ 43 | { 44 | body: "", 45 | hash: "0000000000000000000000000000000000000000", 46 | subject: "fix(expect): fix the function signature of \`toMatchObject()\` (#4202)", 47 | tag: "fix", 48 | }, 49 | ], 50 | module: "expect", 51 | version: "patch", 52 | }, 53 | { 54 | commits: [ 55 | { 56 | body: "", 57 | hash: "0000000000000000000000000000000000000000", 58 | subject: "fix(flags): correct deprecation notices (#4207)", 59 | tag: "fix", 60 | }, 61 | ], 62 | module: "flags", 63 | version: "patch", 64 | }, 65 | { 66 | commits: [ 67 | { 68 | body: "", 69 | hash: "0000000000000000000000000000000000000000", 70 | subject: "fix(fmt): correct \`stripColor()\` deprecation notice (#4208)", 71 | tag: "fix", 72 | }, 73 | ], 74 | module: "fmt", 75 | version: "patch", 76 | }, 77 | { 78 | commits: [ 79 | { 80 | body: "", 81 | hash: "0000000000000000000000000000000000000000", 82 | subject: "BREAKING(http): remove \`CookieMap\` (#4179)", 83 | tag: "BREAKING", 84 | }, 85 | { 86 | body: "", 87 | hash: "0000000000000000000000000000000000000000", 88 | subject: "feat(tools,log,http,semver): check mod exports, export items consistently from mod.ts (#4229)", 89 | tag: "feat", 90 | }, 91 | { 92 | body: "", 93 | hash: "0000000000000000000000000000000000000000", 94 | subject: "docs(http): complete documentation (#4209)", 95 | tag: "docs", 96 | }, 97 | ], 98 | module: "http", 99 | version: "major", 100 | }, 101 | { 102 | commits: [ 103 | { 104 | body: "", 105 | hash: "0000000000000000000000000000000000000000", 106 | subject: "BREAKING(io): remove \`types.d.ts\` (#4237)", 107 | tag: "BREAKING", 108 | }, 109 | { 110 | body: "", 111 | hash: "0000000000000000000000000000000000000000", 112 | subject: "feat(io): un-deprecate \`Buffer\` (#4184)", 113 | tag: "feat", 114 | }, 115 | ], 116 | module: "io", 117 | version: "major", 118 | }, 119 | { 120 | commits: [ 121 | { 122 | body: "* BREAKING(log): remove \`handlers.ts\` 123 | 124 | * fix 125 | 126 | * BREAKING(log): remove string formatter", 127 | hash: "0000000000000000000000000000000000000000", 128 | subject: "BREAKING(log): remove string formatter (#4239)", 129 | tag: "BREAKING", 130 | }, 131 | { 132 | body: "", 133 | hash: "0000000000000000000000000000000000000000", 134 | subject: "BREAKING(log): single-export handler files (#4236)", 135 | tag: "BREAKING", 136 | }, 137 | { 138 | body: "", 139 | hash: "0000000000000000000000000000000000000000", 140 | subject: "feat(tools,log,http,semver): check mod exports, export items consistently from mod.ts (#4229)", 141 | tag: "feat", 142 | }, 143 | { 144 | body: "", 145 | hash: "0000000000000000000000000000000000000000", 146 | subject: "feat(log): make handlers disposable (#4195)", 147 | tag: "feat", 148 | }, 149 | { 150 | body: "", 151 | hash: "0000000000000000000000000000000000000000", 152 | subject: "fix(log): make \`flattenArgs()\` private (#4214)", 153 | tag: "fix", 154 | }, 155 | { 156 | body: "", 157 | hash: "0000000000000000000000000000000000000000", 158 | subject: "refactor(log): tidy imports and exports (#4215)", 159 | tag: "refactor", 160 | }, 161 | { 162 | body: "", 163 | hash: "0000000000000000000000000000000000000000", 164 | subject: "refactor(log): replace deprecated imports (#4188)", 165 | tag: "refactor", 166 | }, 167 | ], 168 | module: "log", 169 | version: "major", 170 | }, 171 | { 172 | commits: [ 173 | { 174 | body: "", 175 | hash: "0000000000000000000000000000000000000000", 176 | subject: "docs(media_types): complete documentation (#4219)", 177 | tag: "docs", 178 | }, 179 | ], 180 | module: "media_types", 181 | version: "patch", 182 | }, 183 | { 184 | commits: [ 185 | { 186 | body: "", 187 | hash: "0000000000000000000000000000000000000000", 188 | subject: "docs(msgpack): complete documentation (#4220)", 189 | tag: "docs", 190 | }, 191 | ], 192 | module: "msgpack", 193 | version: "patch", 194 | }, 195 | { 196 | commits: [ 197 | { 198 | body: "", 199 | hash: "0000000000000000000000000000000000000000", 200 | subject: "deprecation(path): split off all constants into their own files and deprecate old names (#4153)", 201 | tag: "deprecation", 202 | }, 203 | ], 204 | module: "path", 205 | version: "patch", 206 | }, 207 | { 208 | commits: [ 209 | { 210 | body: "", 211 | hash: "0000000000000000000000000000000000000000", 212 | subject: "BREAKING(semver): remove \`FormatStyle\` (#4182)", 213 | tag: "BREAKING", 214 | }, 215 | { 216 | body: "", 217 | hash: "0000000000000000000000000000000000000000", 218 | subject: "BREAKING(semver): remove \`compareBuild()\` (#4181)", 219 | tag: "BREAKING", 220 | }, 221 | { 222 | body: "", 223 | hash: "0000000000000000000000000000000000000000", 224 | subject: "BREAKING(semver): remove \`rsort()\` (#4180)", 225 | tag: "BREAKING", 226 | }, 227 | { 228 | body: "", 229 | hash: "0000000000000000000000000000000000000000", 230 | subject: "feat(tools,log,http,semver): check mod exports, export items consistently from mod.ts (#4229)", 231 | tag: "feat", 232 | }, 233 | { 234 | body: "", 235 | hash: "0000000000000000000000000000000000000000", 236 | subject: "deprecation(semver): rename \`eq()\`, \`neq()\`, \`lt()\`, \`lte()\`, \`gt()\` and \`gte()\` (#4083)", 237 | tag: "deprecation", 238 | }, 239 | { 240 | body: "", 241 | hash: "0000000000000000000000000000000000000000", 242 | subject: "deprecation(semver): deprecate \`SemVerRange\`, introduce \`Range\` (#4161)", 243 | tag: "deprecation", 244 | }, 245 | { 246 | body: "", 247 | hash: "0000000000000000000000000000000000000000", 248 | subject: "deprecation(semver): deprecate \`outside()\` (#4185)", 249 | tag: "deprecation", 250 | }, 251 | { 252 | body: "", 253 | hash: "0000000000000000000000000000000000000000", 254 | subject: "refactor(semver): replace \`parseComparator()\` with comparator objects (#4204)", 255 | tag: "refactor", 256 | }, 257 | ], 258 | module: "semver", 259 | version: "major", 260 | }, 261 | { 262 | commits: [ 263 | { 264 | body: "", 265 | hash: "0000000000000000000000000000000000000000", 266 | subject: "BREAKING(streams): remove \`readAll()\`, \`writeAll()\` and \`copy()\` (#4238)", 267 | tag: "BREAKING", 268 | }, 269 | { 270 | body: "", 271 | hash: "0000000000000000000000000000000000000000", 272 | subject: "feat(streams)!: remove \`readAll()\`, \`writeAll()\` and \`copy()\` (#4238)", 273 | tag: "BREAKING", 274 | }, 275 | { 276 | body: "", 277 | hash: "0000000000000000000000000000000000000000", 278 | subject: "docs(streams): remove \`Deno.metrics()\` use in example (#4217)", 279 | tag: "docs", 280 | }, 281 | ], 282 | module: "streams", 283 | version: "major", 284 | }, 285 | { 286 | commits: [ 287 | { 288 | body: "", 289 | hash: "0000000000000000000000000000000000000000", 290 | subject: "fix(toml): \`parse()\` duplicates the character next to reserved escape sequences (#4192)", 291 | tag: "fix", 292 | }, 293 | { 294 | body: "", 295 | hash: "0000000000000000000000000000000000000000", 296 | subject: "docs(toml): complete documentation (#4223)", 297 | tag: "docs", 298 | }, 299 | { 300 | body: "", 301 | hash: "0000000000000000000000000000000000000000", 302 | subject: "test(toml): improve test coverage (#4211)", 303 | tag: "test", 304 | }, 305 | ], 306 | module: "toml", 307 | version: "patch", 308 | }, 309 | { 310 | commits: [ 311 | { 312 | body: "", 313 | hash: "0000000000000000000000000000000000000000", 314 | subject: "feat(tools,log,http,semver): check mod exports, export items consistently from mod.ts (#4229)", 315 | tag: "feat", 316 | }, 317 | ], 318 | module: "tools", 319 | version: "minor", 320 | }, 321 | { 322 | commits: [ 323 | { 324 | body: "", 325 | hash: "0000000000000000000000000000000000000000", 326 | subject: "refactor(using): use \`using\` keyword for Explicit Resource Management (#4143)", 327 | tag: "refactor", 328 | }, 329 | ], 330 | module: "using", 331 | version: "patch", 332 | }, 333 | { 334 | commits: [ 335 | { 336 | body: "", 337 | hash: "0000000000000000000000000000000000000000", 338 | subject: "refactor(webgpu): use internal \`Deno.close()\` for cleanup of WebGPU resources (#4231)", 339 | tag: "refactor", 340 | }, 341 | ], 342 | module: "webgpu", 343 | version: "patch", 344 | }, 345 | ] 346 | `; 347 | 348 | snapshot[`getWorkspaceModules() 1`] = ` 349 | [ 350 | { 351 | [Symbol(path)]: "testdata/basic/foo/deno.json", 352 | name: "@scope/foo", 353 | version: "1.2.3", 354 | }, 355 | { 356 | [Symbol(path)]: "testdata/basic/bar/deno.json", 357 | name: "@scope/bar", 358 | version: "2.3.4", 359 | }, 360 | { 361 | [Symbol(path)]: "testdata/basic/baz/deno.json", 362 | name: "@scope/baz", 363 | version: "0.2.3", 364 | }, 365 | { 366 | [Symbol(path)]: "testdata/basic/qux/deno.jsonc", 367 | name: "@scope/qux", 368 | version: "0.3.4", 369 | }, 370 | { 371 | [Symbol(path)]: "testdata/basic/quux/deno.json", 372 | name: "@scope/quux", 373 | version: "0.0.0", 374 | }, 375 | ] 376 | `; 377 | 378 | snapshot[`createReleaseNote() 1`] = ` 379 | "### 1970.01.01 380 | 381 | #### @std/collections 0.213.1 (patch) 382 | - feat(collections): pass \`key\` to \`mapValues()\` transformer (#4127) 383 | 384 | #### @std/console 0.213.1 (patch) 385 | - refactor(console): rename \`_rle\` to \`_run_length.ts\` (#4212) 386 | 387 | #### @std/crypto 0.213.1 (patch) 388 | - chore(crypto): upgrade to \`rust@1.75.0\` and \`wasmbuild@0.15.5\` (#4193) 389 | 390 | #### @std/expect 0.213.1 (patch) 391 | - fix(expect): fix the function signature of \`toMatchObject()\` (#4202) 392 | 393 | #### @std/flags 0.213.1 (patch) 394 | - fix(flags): correct deprecation notices (#4207) 395 | 396 | #### @std/fmt 0.213.1 (patch) 397 | - fix(fmt): correct \`stripColor()\` deprecation notice (#4208) 398 | 399 | #### @std/http 0.214.0 (minor) 400 | - BREAKING(http): remove \`CookieMap\` (#4179) 401 | - feat(tools,log,http,semver): check mod exports, export items consistently from mod.ts (#4229) 402 | - docs(http): complete documentation (#4209) 403 | 404 | #### @std/io 0.214.0 (minor) 405 | - BREAKING(io): remove \`types.d.ts\` (#4237) 406 | - feat(io): un-deprecate \`Buffer\` (#4184) 407 | 408 | #### @std/log 0.214.0 (minor) 409 | - BREAKING(log): remove string formatter (#4239) 410 | - BREAKING(log): single-export handler files (#4236) 411 | - feat(tools,log,http,semver): check mod exports, export items consistently from mod.ts (#4229) 412 | - feat(log): make handlers disposable (#4195) 413 | - fix(log): make \`flattenArgs()\` private (#4214) 414 | - refactor(log): tidy imports and exports (#4215) 415 | - refactor(log): replace deprecated imports (#4188) 416 | 417 | #### @std/media_types 0.213.1 (patch) 418 | - docs(media_types): complete documentation (#4219) 419 | 420 | #### @std/msgpack 0.213.1 (patch) 421 | - docs(msgpack): complete documentation (#4220) 422 | 423 | #### @std/path 0.213.1 (patch) 424 | - deprecation(path): split off all constants into their own files and deprecate old names (#4153) 425 | 426 | #### @std/semver 0.214.0 (minor) 427 | - BREAKING(semver): remove \`FormatStyle\` (#4182) 428 | - BREAKING(semver): remove \`compareBuild()\` (#4181) 429 | - BREAKING(semver): remove \`rsort()\` (#4180) 430 | - feat(tools,log,http,semver): check mod exports, export items consistently from mod.ts (#4229) 431 | - deprecation(semver): rename \`eq()\`, \`neq()\`, \`lt()\`, \`lte()\`, \`gt()\` and \`gte()\` (#4083) 432 | - deprecation(semver): deprecate \`SemVerRange\`, introduce \`Range\` (#4161) 433 | - deprecation(semver): deprecate \`outside()\` (#4185) 434 | - refactor(semver): replace \`parseComparator()\` with comparator objects (#4204) 435 | 436 | #### @std/streams 0.214.0 (minor) 437 | - BREAKING(streams): remove \`readAll()\`, \`writeAll()\` and \`copy()\` (#4238) 438 | - feat(streams)!: remove \`readAll()\`, \`writeAll()\` and \`copy()\` (#4238) 439 | - docs(streams): remove \`Deno.metrics()\` use in example (#4217) 440 | 441 | #### @std/toml 0.213.1 (patch) 442 | - fix(toml): \`parse()\` duplicates the character next to reserved escape sequences (#4192) 443 | - docs(toml): complete documentation (#4223) 444 | - test(toml): improve test coverage (#4211) 445 | 446 | #### @std/webgpu 0.213.1 (patch) 447 | - refactor(webgpu): use internal \`Deno.close()\` for cleanup of WebGPU resources (#4231) 448 | " 449 | `; 450 | 451 | snapshot[`createPrBody() 1`] = ` 452 | "The following updates are detected: 453 | 454 | | module | from | to | type | 455 | |----------|---------|---------|-------| 456 | |collections|0.213.0|0.213.1|patch| 457 | |console|0.213.0|0.213.1|patch| 458 | |crypto|0.213.0|0.213.1|patch| 459 | |expect|0.213.0|0.213.1|patch| 460 | |flags|0.213.0|0.213.1|patch| 461 | |fmt|0.213.0|0.213.1|patch| 462 | |http|0.213.0|0.214.0|minor| 463 | |io|0.213.0|0.214.0|minor| 464 | |log|0.213.0|0.214.0|minor| 465 | |media_types|0.213.0|0.213.1|patch| 466 | |msgpack|0.213.0|0.213.1|patch| 467 | |path|0.213.0|0.213.1|patch| 468 | |semver|0.213.0|0.214.0|minor| 469 | |streams|0.213.0|0.214.0|minor| 470 | |toml|0.213.0|0.213.1|patch| 471 | |webgpu|0.213.0|0.213.1|patch| 472 | 473 | Please ensure: 474 | - [ ] Versions in deno.json files are updated correctly 475 | - [ ] Releases.md is updated correctly 476 | 477 | 478 | 479 | The following commits have unknown scopes. Please handle them manually if necessary: 480 | 481 | - [feat(tools,log,http,semver): check mod exports, export items consistently from mod.ts (#4229)](/denoland/deno_std/commit/0000000000000000000000000000000000000000) 482 | - [refactor(using): use \`using\` keyword for Explicit Resource Management (#4143)](/denoland/deno_std/commit/0000000000000000000000000000000000000000) 483 | 484 | 485 | 486 | 487 | 488 | --- 489 | 490 | To make edits to this PR: 491 | 492 | \`\`\`sh 493 | git fetch upstream release-1970-01-01-00-00-00 && git checkout -b release-1970-01-01-00-00-00 upstream/release-1970-01-01-00-00-00 494 | \`\`\` 495 | " 496 | `; 497 | -------------------------------------------------------------------------------- /cli.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2024 the Deno authors. All rights reserved. MIT license. 2 | 3 | import { parseArgs } from "@std/cli/parse-args"; 4 | import { bumpWorkspaces } from "./mod.ts"; 5 | 6 | /** 7 | * The CLI entrypoint of the package. You can directly perform the version bump behavior from CLI: 8 | * 9 | * ```sh 10 | * deno run -A jsr:@deno/bump-workspaces/cli 11 | * ``` 12 | * 13 | * The endpoint supports --dry-run option: 14 | * 15 | * ```sh 16 | * deno run -A jsr:@deno/bump-workspaces/cli --dry-run 17 | * ``` 18 | * 19 | * You can specify import map path by `--import-map` option (Default is deno.json(c) at the root): 20 | * 21 | * ```sh 22 | * deno run -A jsr:@deno/bump-workspaces/cli --import-map ./import_map.json 23 | * ``` 24 | * 25 | * @module 26 | */ 27 | 28 | if (import.meta.main) { 29 | const args = parseArgs(Deno.args, { 30 | string: ["import-map"], 31 | boolean: ["dry-run"], 32 | }); 33 | await bumpWorkspaces({ 34 | dryRun: args["dry-run"], 35 | importMap: args["import-map"], 36 | }); 37 | } 38 | -------------------------------------------------------------------------------- /deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "lock": false, 3 | "name": "@deno/bump-workspaces", 4 | "exports": { 5 | ".": "./mod.ts", 6 | "./cli": "./cli.ts" 7 | }, 8 | "version": "0.1.22", 9 | "tasks": { 10 | "test": "deno test -A", 11 | "cov": "rm -rf coverage && deno test -A --coverage && deno coverage --html && deno coverage --lcov --output=coverage/lcov.info", 12 | "cov-open": "deno task cov && open coverage/html/index.html" 13 | }, 14 | "imports": { 15 | "@david/dax": "jsr:@david/dax@^0.40.1", 16 | "@std/assert": "jsr:@std/assert@^0.224.0", 17 | "@std/cli": "jsr:@std/cli@^0.224.0", 18 | "@std/fmt": "jsr:@std/fmt@^0.224.0", 19 | "@std/fs": "jsr:@std/fs@^0.224.0", 20 | "@std/jsonc": "jsr:@std/jsonc@^0.224.0", 21 | "@std/path": "jsr:@std/path@^0.224.0", 22 | "@std/semver": "jsr:@std/semver@^0.224.0", 23 | "@std/testing": "jsr:@std/testing@^0.224.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /mod.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2024 the Deno authors. All rights reserved. MIT license. 2 | 3 | import { $ } from "@david/dax"; 4 | import { Octokit } from "npm:octokit@^3.1"; 5 | import { cyan, magenta } from "@std/fmt/colors"; 6 | import { ensureFile } from "@std/fs/ensure-file"; 7 | import { join } from "@std/path/join"; 8 | 9 | /** 10 | * Upgrade the versions of the packages in the workspace using Conventional Commits rules. 11 | * 12 | * The workflow of this function is: 13 | * - Read workspace info from the deno.json in the given `root`. 14 | * - Read commit messages between the given `start` and `base`. 15 | * - `start` defaults to the latest tag in the current branch (=`git describe --tags --abbrev=0`) 16 | * - `base` defaults to the current branch (=`git branch --show-current`) 17 | * - Detect necessary version updates from the commit messages. 18 | * - Update the versions in the deno.json files. 19 | * - Create a release note. 20 | * - Create a git commit with given `gitUserName` and `gitUserEmail`. 21 | * - Create a pull request, targeting the given `base` branch. 22 | * 23 | * @module 24 | */ 25 | 26 | import { 27 | applyVersionBump, 28 | checkModuleName, 29 | type Commit, 30 | createPrBody, 31 | createReleaseBranchName, 32 | createReleaseNote, 33 | createReleaseTitle, 34 | defaultParseCommitMessage, 35 | type Diagnostic, 36 | getModule, 37 | getWorkspaceModules, 38 | summarizeVersionBumpsByModule, 39 | type VersionBump, 40 | type VersionUpdateResult, 41 | type WorkspaceModule, 42 | } from "./util.ts"; 43 | 44 | // A random separator that is unlikely to be in a commit message. 45 | const separator = "#%$".repeat(35); 46 | 47 | /** The option for {@linkcode bumpWorkspaces} */ 48 | export type BumpWorkspaceOptions = { 49 | /** The git tag or commit hash to start from. The default is the latest tag. */ 50 | start?: string; 51 | /** The base branch name to compare commits. The default is the current branch. */ 52 | base?: string; 53 | parseCommitMessage?: ( 54 | commit: Commit, 55 | workspaceModules: WorkspaceModule[], 56 | ) => VersionBump[] | Diagnostic; 57 | /** The root directory of the workspace. */ 58 | root?: string; 59 | /** The git user name which is used for making a commit */ 60 | gitUserName?: string; 61 | /** The git user email which is used for making a commit */ 62 | gitUserEmail?: string; 63 | /** The github token e.g. */ 64 | githubToken?: string; 65 | /** The github repository e.g. denoland/deno_std */ 66 | githubRepo?: string; 67 | /** Perform all operations if false. 68 | * Doesn't perform file edits and network operations when true. 69 | * Perform fs ops, but doesn't perform git operations when "network" */ 70 | dryRun?: boolean | "git"; 71 | /** The import map path. Default is deno.json(c) at the root. */ 72 | importMap?: string; 73 | /** The path to release note markdown file. The dfault is `Releases.md` */ 74 | releaseNotePath?: string; 75 | }; 76 | 77 | /** 78 | * Upgrade the versions of the packages in the workspace using Conventional Commits rules. 79 | * 80 | * The workflow of this function is: 81 | * - Read workspace info from the deno.json in the given `root`. 82 | * - Read commit messages between the given `start` and `base`. 83 | * - `start` defaults to the latest tag in the current branch (=`git describe --tags --abbrev=0`) 84 | * - `base` defaults to the current branch (=`git branch --show-current`) 85 | * - Detect necessary version updates from the commit messages. 86 | * - Update the versions in the deno.json files. 87 | * - Create a release note. 88 | * - Create a git commit with given `gitUserName` and `gitUserEmail`. 89 | * - Create a pull request, targeting the given `base` branch. 90 | */ 91 | export async function bumpWorkspaces( 92 | { 93 | parseCommitMessage = defaultParseCommitMessage, 94 | start, 95 | base, 96 | gitUserName, 97 | gitUserEmail, 98 | githubToken, 99 | githubRepo, 100 | dryRun = false, 101 | importMap, 102 | releaseNotePath = "Releases.md", 103 | root = ".", 104 | }: BumpWorkspaceOptions = {}, 105 | ) { 106 | const now = new Date(); 107 | start ??= await $`git describe --tags --abbrev=0`.text(); 108 | base ??= await $`git branch --show-current`.text(); 109 | if (!base) { 110 | console.error("The current branch is not found."); 111 | Deno.exit(1); 112 | } 113 | 114 | await $`git checkout ${start}`; 115 | const [_oldConfigPath, oldModules] = await getWorkspaceModules(root); 116 | await $`git checkout -`; 117 | await $`git checkout ${base}`; 118 | const [configPath, modules] = await getWorkspaceModules(root); 119 | await $`git checkout -`; 120 | 121 | const newBranchName = createReleaseBranchName(now); 122 | releaseNotePath = join(root, releaseNotePath); 123 | 124 | const text = 125 | await $`git --no-pager log --pretty=format:${separator}%H%B ${start}..${base}` 126 | .text(); 127 | 128 | const commits = text.split(separator).map((commit) => { 129 | const hash = commit.slice(0, 40); 130 | commit = commit.slice(40); 131 | const i = commit.indexOf("\n"); 132 | if (i < 0) { 133 | return { hash, subject: commit.trim(), body: "" }; 134 | } 135 | const subject = commit.slice(0, i).trim(); 136 | const body = commit.slice(i + 1).trim(); 137 | return { hash, subject, body }; 138 | }); 139 | commits.shift(); // drop the first empty item 140 | 141 | console.log( 142 | `Found ${cyan(commits.length.toString())} commits between ${ 143 | magenta(start) 144 | } and ${magenta(base)}.`, 145 | ); 146 | const versionBumps: VersionBump[] = []; 147 | const diagnostics: Diagnostic[] = []; 148 | for (const commit of commits) { 149 | if (/^v?\d+\.\d+\.\d+/.test(commit.subject)) { 150 | // Skip if the commit subject is version bump 151 | continue; 152 | } 153 | if (/^Release \d+\.\d+\.\d+/.test(commit.subject)) { 154 | // Skip if the commit subject is release 155 | continue; 156 | } 157 | const parsed = parseCommitMessage(commit, modules); 158 | if (Array.isArray(parsed)) { 159 | for (const versionBump of parsed) { 160 | const diagnostic = checkModuleName(versionBump, modules); 161 | if (diagnostic) { 162 | diagnostics.push(diagnostic); 163 | } else { 164 | versionBumps.push(versionBump); 165 | } 166 | } 167 | } else { 168 | // The commit message is completely unknown 169 | diagnostics.push(parsed); 170 | } 171 | } 172 | const summaries = summarizeVersionBumpsByModule(versionBumps); 173 | 174 | if (summaries.length === 0) { 175 | console.log("No version bumps."); 176 | return; 177 | } 178 | 179 | console.log(`Updating the versions:`); 180 | let importMapPath: string; 181 | if (importMap) { 182 | console.log(`Using the import map: ${cyan(importMap)}`); 183 | importMapPath = importMap; 184 | } else { 185 | importMapPath = configPath; 186 | } 187 | const updates: Record = {}; 188 | let importMapJson = await Deno.readTextFile(importMapPath); 189 | for (const summary of summaries) { 190 | const module = getModule(summary.module, modules)!; 191 | const oldModule = getModule(summary.module, oldModules); 192 | const [importMapJson_, versionUpdate] = await applyVersionBump( 193 | summary, 194 | module, 195 | oldModule, 196 | importMapJson, 197 | dryRun === true, 198 | ); 199 | importMapJson = importMapJson_; 200 | updates[module.name] = versionUpdate; 201 | } 202 | console.table(updates, ["diff", "from", "to", "path"]); 203 | 204 | console.log( 205 | `Found ${cyan(diagnostics.length.toString())} diagnostics:`, 206 | ); 207 | for (const unknownCommit of diagnostics) { 208 | console.log(` ${unknownCommit.type} ${unknownCommit.commit.subject}`); 209 | } 210 | 211 | const releaseNote = createReleaseNote(Object.values(updates), modules, now); 212 | 213 | if (dryRun === true) { 214 | console.log(); 215 | console.log(cyan("The release note:")); 216 | console.log(releaseNote); 217 | console.log(cyan("Skip making a commit.")); 218 | console.log(cyan("Skip making a pull request.")); 219 | } else { 220 | // Updates deno.json 221 | await Deno.writeTextFile(importMapPath, importMapJson); 222 | 223 | // Prepend release notes 224 | await ensureFile(releaseNotePath); 225 | await Deno.writeTextFile( 226 | releaseNotePath, 227 | releaseNote + "\n" + await Deno.readTextFile(releaseNotePath), 228 | ); 229 | 230 | await $`deno fmt ${releaseNotePath}`; 231 | 232 | if (dryRun === false) { 233 | gitUserName ??= Deno.env.get("GIT_USER_NAME"); 234 | if (gitUserName === undefined) { 235 | console.error("GIT_USER_NAME is not set."); 236 | Deno.exit(1); 237 | } 238 | gitUserEmail ??= Deno.env.get("GIT_USER_EMAIL"); 239 | if (gitUserEmail === undefined) { 240 | console.error("GIT_USER_EMAIL is not set."); 241 | Deno.exit(1); 242 | } 243 | githubToken ??= Deno.env.get("GITHUB_TOKEN"); 244 | if (githubToken === undefined) { 245 | console.error("GITHUB_TOKEN is not set."); 246 | Deno.exit(1); 247 | } 248 | githubRepo ??= Deno.env.get("GITHUB_REPOSITORY"); 249 | if (githubRepo === undefined) { 250 | console.error("GITHUB_REPOSITORY is not set."); 251 | Deno.exit(1); 252 | } 253 | 254 | // Makes a commit 255 | console.log( 256 | `Creating a git commit in the new branch ${magenta(newBranchName)}.`, 257 | ); 258 | await $`git checkout -b ${newBranchName}`; 259 | await $`git add .`; 260 | await $`git -c "user.name=${gitUserName}" -c "user.email=${gitUserEmail}" commit -m "chore: update versions"`; 261 | 262 | console.log(`Pushing the new branch ${magenta(newBranchName)}.`); 263 | await $`git push origin ${newBranchName}`; 264 | 265 | // Makes a PR 266 | console.log(`Creating a pull request.`); 267 | const octoKit = new Octokit({ auth: githubToken }); 268 | const [owner, repo] = githubRepo.split("/"); 269 | const openedPr = await octoKit.request( 270 | "POST /repos/{owner}/{repo}/pulls", 271 | { 272 | owner, 273 | repo, 274 | base: base, 275 | head: newBranchName, 276 | draft: true, 277 | title: `chore: release ${createReleaseTitle(now)}`, 278 | body: createPrBody( 279 | Object.values(updates), 280 | diagnostics, 281 | githubRepo, 282 | newBranchName, 283 | ), 284 | }, 285 | ); 286 | console.log("New pull request:", cyan(openedPr.data.html_url)); 287 | } 288 | 289 | console.log("Done."); 290 | } 291 | } 292 | -------------------------------------------------------------------------------- /mod_test.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2024 the Deno authors. All rights reserved. MIT license. 2 | 3 | import { assertSnapshot } from "@std/testing/snapshot"; 4 | import { copy, exists } from "@std/fs"; 5 | import { bumpWorkspaces } from "./mod.ts"; 6 | import { join } from "@std/path"; 7 | import { tryGetDenoConfig } from "./util.ts"; 8 | import { assert, assertEquals } from "@std/assert"; 9 | 10 | // Note: The test cases in this file use git information in the branch `origin/base-branch-for-testing`. 11 | 12 | Deno.test("bumpWorkspaces()", async (t) => { 13 | const dir = await Deno.makeTempDir(); 14 | await copy("testdata/basic", dir, { overwrite: true }); 15 | await bumpWorkspaces({ 16 | dryRun: "git", 17 | githubRepo: "denoland/deno_std", 18 | githubToken: "1234567890", 19 | base: "origin/base-branch-for-testing", 20 | start: "start-tag-for-testing", 21 | root: dir, 22 | }); 23 | 24 | const releaseNote = await Deno.readTextFile(join(dir, "Releases.md")); 25 | await assertSnapshot( 26 | t, 27 | releaseNote.replace(/^### \d+\.\d+\.\d+/, "### YYYY.MM.DD"), 28 | ); 29 | 30 | let _, config; 31 | [_, config] = await tryGetDenoConfig(dir); 32 | assertEquals(config, { 33 | imports: { 34 | "@scope/foo": "jsr:@scope/foo@^2.0.0", 35 | "@scope/foo/": "jsr:@scope/foo@^2.0.0/", 36 | "@scope/bar": "jsr:@scope/bar@^2.3.5", 37 | "@scope/bar/": "jsr:@scope/bar@^2.3.5/", 38 | "@scope/baz": "jsr:@scope/baz@^0.2.4", 39 | "@scope/baz/": "jsr:@scope/baz@^0.2.4/", 40 | "@scope/qux": "jsr:@scope/qux@^0.3.5", 41 | "@scope/qux/": "jsr:@scope/qux@^0.3.5/", 42 | "@scope/quux": "jsr:@scope/quux@^0.1.0", 43 | "@scope/quux/": "jsr:@scope/quux@^0.1.0/", 44 | }, 45 | workspace: ["./foo", "./bar", "./baz", "./qux", "./quux"], 46 | }); 47 | [_, config] = await tryGetDenoConfig(join(dir, "foo")); 48 | assertEquals(config, { 49 | name: "@scope/foo", 50 | version: "2.0.0", 51 | }); 52 | [_, config] = await tryGetDenoConfig(join(dir, "bar")); 53 | assertEquals(config, { 54 | name: "@scope/bar", 55 | version: "2.3.5", 56 | }); 57 | [_, config] = await tryGetDenoConfig(join(dir, "baz")); 58 | assertEquals(config, { 59 | name: "@scope/baz", 60 | version: "0.2.4", 61 | }); 62 | [_, config] = await tryGetDenoConfig(join(dir, "qux")); 63 | assertEquals(config, { 64 | name: "@scope/qux", 65 | version: "0.3.5", 66 | }); 67 | [_, config] = await tryGetDenoConfig(join(dir, "quux")); 68 | assertEquals(config, { 69 | name: "@scope/quux", 70 | version: "0.1.0", 71 | }); 72 | }); 73 | 74 | Deno.test( 75 | "bumpWorkspaces() doesn't write things when dry run specified", 76 | async () => { 77 | const dir = await Deno.makeTempDir(); 78 | await copy("testdata/basic", dir, { overwrite: true }); 79 | await bumpWorkspaces({ 80 | dryRun: true, 81 | githubRepo: "denoland/deno_std", 82 | githubToken: "1234567890", 83 | base: "origin/base-branch-for-testing", 84 | start: "start-tag-for-testing", 85 | root: dir, 86 | }); 87 | 88 | assert(!(await exists(join(dir, "Releases.md")))); 89 | 90 | const [_, config] = await tryGetDenoConfig(dir); 91 | assertEquals(config, { 92 | imports: { 93 | "@scope/foo": "jsr:@scope/foo@^1.2.3", 94 | "@scope/foo/": "jsr:@scope/foo@^1.2.3/", 95 | "@scope/bar": "jsr:@scope/bar@^2.3.4", 96 | "@scope/bar/": "jsr:@scope/bar@^2.3.4/", 97 | "@scope/baz": "jsr:@scope/baz@^0.2.3", 98 | "@scope/baz/": "jsr:@scope/baz@^0.2.3/", 99 | "@scope/qux": "jsr:@scope/qux@^0.3.4", 100 | "@scope/qux/": "jsr:@scope/qux@^0.3.4/", 101 | "@scope/quux": "jsr:@scope/quux@^0.0.0", 102 | "@scope/quux/": "jsr:@scope/quux@^0.0.0/", 103 | }, 104 | workspace: ["./foo", "./bar", "./baz", "./qux", "./quux"], 105 | }); 106 | }, 107 | ); 108 | -------------------------------------------------------------------------------- /testdata/basic/bar/deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@scope/bar", 3 | "version": "2.3.4" 4 | } 5 | -------------------------------------------------------------------------------- /testdata/basic/baz/deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@scope/baz", 3 | "version": "0.2.3" 4 | } 5 | -------------------------------------------------------------------------------- /testdata/basic/deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "imports": { 3 | "@scope/foo": "jsr:@scope/foo@^1.2.3", 4 | "@scope/foo/": "jsr:@scope/foo@^1.2.3/", 5 | "@scope/bar": "jsr:@scope/bar@^2.3.4", 6 | "@scope/bar/": "jsr:@scope/bar@^2.3.4/", 7 | "@scope/baz": "jsr:@scope/baz@^0.2.3", 8 | "@scope/baz/": "jsr:@scope/baz@^0.2.3/", 9 | "@scope/qux": "jsr:@scope/qux@^0.3.4", 10 | "@scope/qux/": "jsr:@scope/qux@^0.3.4/", 11 | "@scope/quux": "jsr:@scope/quux@^0.0.0", 12 | "@scope/quux/": "jsr:@scope/quux@^0.0.0/" 13 | }, 14 | "workspace": [ 15 | "./foo", 16 | "./bar", 17 | "./baz", 18 | "./qux", 19 | "./quux" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /testdata/basic/foo/deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@scope/foo", 3 | "version": "1.2.3" 4 | } 5 | -------------------------------------------------------------------------------- /testdata/basic/quux/deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@scope/quux", 3 | "version": "0.0.0" 4 | } 5 | -------------------------------------------------------------------------------- /testdata/basic/qux/deno.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@scope/qux", 3 | "version": "0.3.4" 4 | } 5 | -------------------------------------------------------------------------------- /testdata/std_mock/collections/deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@std/collections", 3 | "version": "0.213.0" 4 | } 5 | -------------------------------------------------------------------------------- /testdata/std_mock/console/deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@std/console", 3 | "version": "0.213.0" 4 | } 5 | -------------------------------------------------------------------------------- /testdata/std_mock/crypto/deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@std/crypto", 3 | "version": "0.213.0" 4 | } 5 | -------------------------------------------------------------------------------- /testdata/std_mock/deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "imports": { 3 | "@std/collections": "jsr:@std/collections@^0.213.0", 4 | "@std/collections/": "jsr:/@std/collections@^0.213.0/", 5 | "@std/crypto": "jsr:@std/crypto@^0.213.0", 6 | "@std/crypto/": "jsr:/@std/crypto@^0.213.0/", 7 | "@std/console": "jsr:@std/console@^0.213.0", 8 | "@std/console/": "jsr:/@std/console@^0.213.0/", 9 | "@std/expect": "jsr:@std/expect@^0.213.1", 10 | "@std/expect/": "jsr:/@std/expect@^0.213.1/", 11 | "@std/flags": "jsr:@std/flags@^0.213.0", 12 | "@std/flags/": "jsr:/@std/flags@^0.213.0/", 13 | "@std/fmt": "jsr:@std/fmt@^0.213.0", 14 | "@std/fmt/": "jsr:/@std/fmt@^0.213.0/", 15 | "@std/http": "jsr:@std/http@^0.214.0", 16 | "@std/http/": "jsr:/@std/http@^0.214.0/", 17 | "@std/io": "jsr:@std/io@^0.214.0", 18 | "@std/io/": "jsr:/@std/io@^0.214.0/", 19 | "@std/log": "jsr:@std/log@^0.214.0", 20 | "@std/log/": "jsr:/@std/log@^0.214.0/", 21 | "@std/media_types": "jsr:@std/media_types@^0.213.0", 22 | "@std/media_types/": "jsr:/@std/media_types@^0.213.0/", 23 | "@std/msgpack": "jsr:@std/msgpack@^0.213.0", 24 | "@std/msgpack/": "jsr:/@std/msgpack@^0.213.0/", 25 | "@std/path": "jsr:@std/path@^0.213.0", 26 | "@std/path/": "jsr:/@std/path@^0.213.0/", 27 | "@std/semver": "jsr:@std/semver@^0.214.0", 28 | "@std/semver/": "jsr:/@std/semver@^0.214.0/", 29 | "@std/streams": "jsr:@std/streams@^0.214.0", 30 | "@std/streams/": "jsr:/@std/streams@^0.214.0/", 31 | "@std/toml": "jsr:@std/toml@^0.213.0", 32 | "@std/toml/": "jsr:/@std/toml@^0.213.0/", 33 | "@std/webgpu": "jsr:@std/webgpu@^0.213.0", 34 | "@std/webgpu/": "jsr:/@std/webgpu@^0.213.0/" 35 | }, 36 | "workspace": [ 37 | "./fmt", 38 | "./io", 39 | "./collections", 40 | "./console", 41 | "./crypto", 42 | "./streams", 43 | "./expect", 44 | "./flags", 45 | "./toml", 46 | "./path", 47 | "./media_types", 48 | "./msgpack", 49 | "./webgpu", 50 | "./http", 51 | "./log", 52 | "./semver" 53 | ] 54 | } 55 | -------------------------------------------------------------------------------- /testdata/std_mock/expect/deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@std/expect", 3 | "version": "0.213.0" 4 | } 5 | -------------------------------------------------------------------------------- /testdata/std_mock/flags/deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@std/flags", 3 | "version": "0.213.0" 4 | } 5 | -------------------------------------------------------------------------------- /testdata/std_mock/fmt/deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@std/fmt", 3 | "version": "0.213.0" 4 | } 5 | -------------------------------------------------------------------------------- /testdata/std_mock/http/deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@std/http", 3 | "version": "0.213.0" 4 | } 5 | -------------------------------------------------------------------------------- /testdata/std_mock/io/deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@std/io", 3 | "version": "0.213.0" 4 | } 5 | -------------------------------------------------------------------------------- /testdata/std_mock/log/deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@std/log", 3 | "version": "0.213.0" 4 | } 5 | -------------------------------------------------------------------------------- /testdata/std_mock/media_types/deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@std/media_types", 3 | "version": "0.213.0" 4 | } 5 | -------------------------------------------------------------------------------- /testdata/std_mock/msgpack/deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@std/msgpack", 3 | "version": "0.213.0" 4 | } 5 | -------------------------------------------------------------------------------- /testdata/std_mock/path/deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@std/path", 3 | "version": "0.213.0" 4 | } 5 | -------------------------------------------------------------------------------- /testdata/std_mock/semver/deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@std/semver", 3 | "version": "0.213.0" 4 | } 5 | -------------------------------------------------------------------------------- /testdata/std_mock/streams/deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@std/streams", 3 | "version": "0.213.0" 4 | } 5 | -------------------------------------------------------------------------------- /testdata/std_mock/toml/deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@std/toml", 3 | "version": "0.213.0" 4 | } 5 | -------------------------------------------------------------------------------- /testdata/std_mock/webgpu/deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@std/webgpu", 3 | "version": "0.213.0" 4 | } 5 | -------------------------------------------------------------------------------- /util.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2024 the Deno authors. All rights reserved. MIT license. 2 | 3 | import { parse as parseJsonc } from "@std/jsonc/parse"; 4 | import { join } from "@std/path/join"; 5 | import { resolve } from "@std/path/resolve"; 6 | import { 7 | format as formatSemver, 8 | increment, 9 | parse as parseSemVer, 10 | type SemVer, 11 | } from "@std/semver"; 12 | import { red } from "@std/fmt/colors"; 13 | 14 | export type VersionUpdate = "major" | "minor" | "patch" | "prerelease"; 15 | 16 | export type Commit = { 17 | subject: string; 18 | body: string; 19 | hash: string; 20 | }; 21 | 22 | export type CommitWithTag = Commit & { tag: string }; 23 | 24 | export const pathProp = Symbol.for("path"); 25 | 26 | export type WorkspaceModule = { 27 | name: string; 28 | version: string; 29 | [pathProp]: string; 30 | }; 31 | 32 | export type VersionBump = { 33 | module: string; 34 | tag: string; 35 | commit: Commit; 36 | version: VersionUpdate; 37 | }; 38 | 39 | export type VersionBumpSummary = { 40 | module: string; 41 | version: VersionUpdate; 42 | commits: CommitWithTag[]; 43 | }; 44 | 45 | export type Diagnostic = 46 | | UnknownCommit 47 | | UnknownRangeCommit 48 | | SkippedCommit 49 | | MissingRange; 50 | 51 | export type UnknownCommit = { 52 | type: "unknown_commit"; 53 | commit: Commit; 54 | reason: string; 55 | }; 56 | 57 | export type MissingRange = { 58 | type: "missing_range"; 59 | commit: Commit; 60 | reason: string; 61 | }; 62 | 63 | export type UnknownRangeCommit = { 64 | type: "unknown_range_commit"; 65 | commit: Commit; 66 | reason: string; 67 | }; 68 | 69 | export type SkippedCommit = { 70 | type: "skipped_commit"; 71 | commit: Commit; 72 | reason: string; 73 | }; 74 | 75 | export type AppliedVersionBump = { 76 | oldVersion: string; 77 | newVersion: string; 78 | diff: VersionUpdate; 79 | denoJson: string; 80 | }; 81 | 82 | export type VersionUpdateResult = { 83 | from: string; 84 | to: string; 85 | diff: VersionUpdate; 86 | path: string; 87 | summary: VersionBumpSummary; 88 | }; 89 | 90 | const RE_DEFAULT_PATTERN = /^([^:()]+)(?:\((.+)\))?(\!)?: (.*)$/; 91 | const REGEXP_UNSTABLE_SCOPE = /^(unstable\/(.+)|(.+)\/unstable)$/; 92 | 93 | type VersionBumpKind = "major" | "minor" | "patch"; 94 | // Defines the version bump for each tag. 95 | const TAG_TO_VERSION: Record = { 96 | BREAKING: "major", 97 | feat: "minor", 98 | deprecation: "patch", 99 | fix: "patch", 100 | perf: "patch", 101 | docs: "patch", 102 | style: "patch", 103 | refactor: "patch", 104 | test: "patch", 105 | chore: "patch", 106 | }; 107 | const POST_MODULE_TO_VERSION: Record = { 108 | "!": "major", 109 | }; 110 | const TAG_PRIORITY = Object.keys(TAG_TO_VERSION); 111 | 112 | export const DEFAULT_RANGE_REQUIRED = [ 113 | "BREAKING", 114 | "feat", 115 | "fix", 116 | "perf", 117 | "deprecation", 118 | ]; 119 | 120 | export function defaultParseCommitMessage( 121 | commit: Commit, 122 | workspaceModules: WorkspaceModule[], 123 | ): VersionBump[] | Diagnostic { 124 | const match = RE_DEFAULT_PATTERN.exec(commit.subject); 125 | if (match === null) { 126 | return { 127 | type: "unknown_commit", 128 | commit, 129 | reason: "The commit message does not match the default pattern.", 130 | }; 131 | } 132 | const [, tag, module, optionalPostModule, _message] = match; 133 | const modules = module === "*" 134 | ? workspaceModules.map((x) => x.name) 135 | : module 136 | ? module.split(/\s*,\s*/) 137 | : []; 138 | if (modules.length === 0) { 139 | if (DEFAULT_RANGE_REQUIRED.includes(tag)) { 140 | return { 141 | type: "missing_range", 142 | commit, 143 | reason: "The commit message does not specify a module.", 144 | }; 145 | } 146 | return { 147 | type: "skipped_commit", 148 | commit, 149 | reason: "The commit message does not specify a module.", 150 | }; 151 | } 152 | const version = optionalPostModule in POST_MODULE_TO_VERSION 153 | ? POST_MODULE_TO_VERSION[optionalPostModule] 154 | : TAG_TO_VERSION[tag]; 155 | if (version === undefined) { 156 | return { 157 | type: "unknown_commit", 158 | commit, 159 | reason: `Unknown commit tag: ${tag}.`, 160 | }; 161 | } 162 | return modules.map((module) => { 163 | const matchUnstable = REGEXP_UNSTABLE_SCOPE.exec(module); 164 | if (matchUnstable) { 165 | // 'scope' is in the form of unstable/foo or foo/unstable 166 | // In this case all changes are considered as patch 167 | return { 168 | module: matchUnstable[2] || matchUnstable[3], 169 | tag, 170 | commit, 171 | version: "patch", 172 | }; 173 | } 174 | return ({ module, tag, version, commit }); 175 | }); 176 | } 177 | 178 | export function summarizeVersionBumpsByModule( 179 | versionBumps: VersionBump[], 180 | ): VersionBumpSummary[] { 181 | const result = {} as Record; 182 | for (const versionBump of versionBumps) { 183 | const { module, version } = versionBump; 184 | const summary = result[module] = result[module] ?? { 185 | module, 186 | version, 187 | commits: [], 188 | }; 189 | summary.version = maxVersion(summary.version, version); 190 | summary.commits.push({ ...versionBump.commit, tag: versionBump.tag }); 191 | } 192 | for (const summary of Object.values(result)) { 193 | summary.commits.sort((a, b) => { 194 | const priorityA = TAG_PRIORITY.indexOf(a.tag); 195 | const priorityB = TAG_PRIORITY.indexOf(b.tag); 196 | if (priorityA === priorityB) { 197 | return 0; 198 | } 199 | return priorityA < priorityB ? -1 : 1; 200 | }); 201 | } 202 | 203 | return Object.values(result).sort((a, b) => a.module < b.module ? -1 : 1); 204 | } 205 | 206 | export function maxVersion( 207 | v0: VersionUpdate, 208 | v1: VersionUpdate, 209 | ): VersionUpdate { 210 | if (v0 === "major" || v1 === "major") { 211 | return "major"; 212 | } 213 | if (v0 === "minor" || v1 === "minor") { 214 | return "minor"; 215 | } 216 | return "patch"; 217 | } 218 | 219 | export async function tryGetDenoConfig( 220 | path: string, 221 | // deno-lint-ignore no-explicit-any 222 | ): Promise<[path: string, config: any]> { 223 | let denoJson: string | undefined; 224 | let denoJsonPath: string | undefined; 225 | try { 226 | denoJsonPath = join(path, "deno.json"); 227 | denoJson = await Deno.readTextFile(denoJsonPath); 228 | } catch (e) { 229 | if (!(e instanceof Deno.errors.NotFound)) { 230 | throw e; 231 | } 232 | } 233 | 234 | if (!denoJson) { 235 | try { 236 | denoJsonPath = join(path, "deno.jsonc"); 237 | denoJson = await Deno.readTextFile(denoJsonPath); 238 | } catch (e) { 239 | if (e instanceof Deno.errors.NotFound) { 240 | console.log(`No deno.json or deno.jsonc found in ${resolve(path)}`); 241 | Deno.exit(1); 242 | } 243 | throw e; 244 | } 245 | } 246 | 247 | try { 248 | return [denoJsonPath!, parseJsonc(denoJson)]; 249 | } catch (e) { 250 | console.log("Invalid deno.json or deno.jsonc file."); 251 | console.log(e); 252 | Deno.exit(1); 253 | } 254 | } 255 | 256 | export async function getWorkspaceModules( 257 | root: string, 258 | ): Promise<[string, WorkspaceModule[]]> { 259 | const [path, denoConfig] = await tryGetDenoConfig(root); 260 | const workspaces = denoConfig.workspaces || denoConfig.workspace; 261 | 262 | if (!Array.isArray(workspaces)) { 263 | console.log(red("Error") + " deno.json doesn't have workspace field."); 264 | Deno.exit(1); 265 | } 266 | 267 | const result = []; 268 | for (const workspace of workspaces) { 269 | if (typeof workspace !== "string") { 270 | console.log("deno.json workspace field should be an array of strings."); 271 | Deno.exit(1); 272 | } 273 | const [path, workspaceConfig] = await tryGetDenoConfig( 274 | join(root, workspace), 275 | ); 276 | if (!workspaceConfig.name) { 277 | continue; 278 | } 279 | result.push({ ...workspaceConfig, [pathProp]: path }); 280 | } 281 | return [path, result]; 282 | } 283 | 284 | export function getModule(module: string, modules: WorkspaceModule[]) { 285 | return modules.find((m) => 286 | m.name === module || m.name.endsWith(`/${module}`) 287 | ); 288 | } 289 | 290 | export function checkModuleName( 291 | versionBump: Pick, 292 | modules: WorkspaceModule[], 293 | ): Diagnostic | undefined { 294 | if (getModule(versionBump.module, modules)) { 295 | return undefined; 296 | } 297 | // The commit include unknown module name 298 | return { 299 | type: "unknown_range_commit", 300 | commit: versionBump.commit, 301 | reason: `Unknown module: ${versionBump.module}.`, 302 | }; 303 | } 304 | 305 | function hasPrerelease(version: SemVer) { 306 | return version.prerelease !== undefined && version.prerelease.length > 0; 307 | } 308 | 309 | export function calcVersionDiff( 310 | newVersionStr: string, 311 | oldVersionStr: string, 312 | ): VersionUpdate { 313 | const newVersion = parseSemVer(newVersionStr); 314 | const oldVersion = parseSemVer(oldVersionStr); 315 | if (hasPrerelease(newVersion)) { 316 | return "prerelease"; 317 | } else if (newVersion.major !== oldVersion.major) { 318 | return "major"; 319 | } else if (newVersion.minor !== oldVersion.minor) { 320 | return "minor"; 321 | } else if (newVersion.patch !== oldVersion.patch) { 322 | return "patch"; 323 | } else if ( 324 | hasPrerelease(oldVersion) && !hasPrerelease(newVersion) && 325 | newVersion.major === oldVersion.major && 326 | newVersion.minor === oldVersion.minor && 327 | newVersion.patch === oldVersion.patch 328 | ) { 329 | // The prerelease version is removed like 330 | // 1.0.0-rc.1 -> 1.0.0 331 | if (newVersion.patch !== 0) { 332 | return "patch"; 333 | } else if (newVersion.minor !== 0) { 334 | return "minor"; 335 | } else if (newVersion.major !== 0) { 336 | return "major"; 337 | } 338 | } 339 | throw new Error( 340 | `Unexpected manual version update: ${oldVersion} -> ${newVersion}`, 341 | ); 342 | } 343 | 344 | /** Apply the version bump to the file system. */ 345 | export async function applyVersionBump( 346 | summary: VersionBumpSummary, 347 | module: WorkspaceModule, 348 | oldModule: WorkspaceModule | undefined, 349 | denoJson: string, 350 | dryRun = false, 351 | ): Promise<[denoJson: string, VersionUpdateResult]> { 352 | if (!oldModule) { 353 | // The module is newly added 354 | console.info(`New module ${module.name} detected.`); 355 | const diff = calcVersionDiff(module.version, "0.0.0"); 356 | summary.version = diff; 357 | return [denoJson, { 358 | from: "0.0.0", 359 | to: module.version, 360 | diff, 361 | summary, 362 | path: module[pathProp], 363 | }]; 364 | } 365 | if (oldModule.version !== module.version) { 366 | // The version is manually updated 367 | console.info( 368 | `Manual version update detected for ${module.name}: ${oldModule.version} -> ${module.version}`, 369 | ); 370 | 371 | const diff = calcVersionDiff(module.version, oldModule.version); 372 | summary.version = diff; 373 | return [denoJson, { 374 | from: oldModule.version, 375 | to: module.version, 376 | diff, 377 | summary, 378 | path: module[pathProp], 379 | }]; 380 | } 381 | const currentVersionStr = module.version; 382 | const currentVersion = parseSemVer(currentVersionStr); 383 | let diff = summary.version; 384 | if (currentVersion.prerelease && currentVersion.prerelease.length > 0) { 385 | // If the current version is a prerelease version, the version bump type is always prerelease 386 | diff = "prerelease"; 387 | } else if (currentVersion.major === 0) { 388 | // Change the version bump type for 0.x.y 389 | // This is aligned with the spec proposal discussed in https://github.com/semver/semver/pull/923 390 | if (diff === "major") { 391 | // breaking change is considered as minor in 0.x.y 392 | diff = "minor"; 393 | } else if (diff === "minor") { 394 | // new feature is considered as patch in 0.x.y 395 | diff = "patch"; 396 | } 397 | } 398 | summary.version = diff; 399 | const newVersion = increment(currentVersion, diff); 400 | const newVersionStr = formatSemver(newVersion); 401 | module.version = newVersionStr; 402 | const path = module[pathProp]; 403 | if (!dryRun) { 404 | await Deno.writeTextFile(path, JSON.stringify(module, null, 2) + "\n"); 405 | } 406 | denoJson = denoJson.replace( 407 | new RegExp(`${module.name}@([^~]?)${currentVersionStr}`, "g"), 408 | `${module.name}@$1${newVersionStr}`, 409 | ); 410 | if (path.endsWith("deno.jsonc")) { 411 | console.warn( 412 | `Currently this tool doesn't keep the comments in deno.jsonc files. Comments in the path "${path}" might be removed by this update.`, 413 | ); 414 | } 415 | return [denoJson, { 416 | from: currentVersionStr, 417 | to: newVersionStr, 418 | diff, 419 | summary, 420 | path, 421 | }]; 422 | } 423 | 424 | export function createReleaseNote( 425 | updates: VersionUpdateResult[], 426 | modules: WorkspaceModule[], 427 | date: Date, 428 | ) { 429 | const heading = `### ${createReleaseTitle(date)}\n\n`; 430 | return heading + updates.map((u) => { 431 | const module = getModule(u.summary.module, modules)!; 432 | return `#### ${module.name} ${u.to} (${u.diff}) \n` + 433 | u.summary.commits.map((c) => `- ${c.subject}\n`).join(""); 434 | }).join("\n"); 435 | } 436 | 437 | export function createPrBody( 438 | updates: VersionUpdateResult[], 439 | diagnostics: Diagnostic[], 440 | githubRepo: string, 441 | releaseBranch: string, 442 | ) { 443 | const table = updates.map((u) => 444 | "|" + [u.summary.module, u.from, u.to, u.diff].join("|") + "|" 445 | ).join("\n"); 446 | 447 | const unknownCommitsNotes = createDiagnosticsNotes( 448 | "The following commits are not recognized. Please handle them manually if necessary:", 449 | "unknown_commit", 450 | ); 451 | const unknownRangesNotes = createDiagnosticsNotes( 452 | "The following commits have unknown scopes. Please handle them manually if necessary:", 453 | "unknown_range_commit", 454 | ); 455 | const missingRangesNotes = createDiagnosticsNotes( 456 | "Required scopes are missing in the following commits. Please handle them manually if necessary:", 457 | "missing_range", 458 | ); 459 | const ignoredCommitsNotes = createDiagnosticsNotes( 460 | "The following commits are ignored:", 461 | "skipped_commit", 462 | ); 463 | return `The following updates are detected: 464 | 465 | | module | from | to | type | 466 | |----------|---------|---------|-------| 467 | ${table} 468 | 469 | Please ensure: 470 | - [ ] Versions in deno.json files are updated correctly 471 | - [ ] Releases.md is updated correctly 472 | 473 | ${unknownCommitsNotes} 474 | 475 | ${unknownRangesNotes} 476 | 477 | ${missingRangesNotes} 478 | 479 | ${ignoredCommitsNotes} 480 | 481 | --- 482 | 483 | To make edits to this PR: 484 | 485 | \`\`\`sh 486 | git fetch upstream ${releaseBranch} && git checkout -b ${releaseBranch} upstream/${releaseBranch} 487 | \`\`\` 488 | `; 489 | function createDiagnosticsNotes( 490 | note: string, 491 | type: string, 492 | ) { 493 | const diagnostics_ = diagnostics.filter((d) => d.type === type); 494 | if (diagnostics_.length === 0) { 495 | return ""; 496 | } 497 | return `${note}\n\n` + 498 | diagnostics_.map((d) => 499 | `- [${d.commit.subject}](/${githubRepo}/commit/${d.commit.hash})` 500 | ).join("\n"); 501 | } 502 | } 503 | 504 | export function createReleaseBranchName(date: Date) { 505 | return "release-" + 506 | date.toISOString().replace("T", "-").replaceAll(":", "-").replace( 507 | /\..+/, 508 | "", 509 | ); 510 | } 511 | 512 | export function createReleaseTitle(d: Date) { 513 | const year = d.getUTCFullYear(); 514 | const month = (d.getUTCMonth() + 1).toString().padStart(2, "0"); 515 | const date = d.getUTCDate().toString().padStart(2, "0"); 516 | return `${year}.${month}.${date}`; 517 | } 518 | -------------------------------------------------------------------------------- /util_test.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2024 the Deno authors. All rights reserved. MIT license. 2 | 3 | import { assertEquals, assertExists, assertObjectMatch } from "@std/assert"; 4 | import { assertSnapshot } from "@std/testing/snapshot"; 5 | import denoJson from "./deno.json" with { type: "json" }; 6 | import { 7 | applyVersionBump, 8 | checkModuleName, 9 | createPrBody, 10 | createReleaseBranchName, 11 | createReleaseNote, 12 | createReleaseTitle, 13 | defaultParseCommitMessage, 14 | type Diagnostic, 15 | getModule, 16 | getWorkspaceModules, 17 | maxVersion, 18 | pathProp, 19 | summarizeVersionBumpsByModule, 20 | type VersionBump, 21 | type WorkspaceModule, 22 | } from "./util.ts"; 23 | import { tryGetDenoConfig } from "./util.ts"; 24 | 25 | const emptyCommit = { 26 | subject: "", 27 | body: "", 28 | hash: "", 29 | } as const; 30 | 31 | const hash = "0000000000000000000000000000000000000000"; 32 | 33 | function parse(subject: string, workspaceModules: WorkspaceModule[]) { 34 | return defaultParseCommitMessage( 35 | { subject, body: "", hash }, 36 | workspaceModules, 37 | ); 38 | } 39 | 40 | Deno.test("defaultParseCommitMessage()", () => { 41 | const modules: WorkspaceModule[] = [ 42 | { name: "foo", version: "0.0.0", [pathProp]: "" }, 43 | { name: "bar", version: "0.0.0", [pathProp]: "" }, 44 | ]; 45 | 46 | assertEquals(parse("feat(foo): add a feature", modules), [ 47 | { 48 | module: "foo", 49 | tag: "feat", 50 | version: "minor", 51 | commit: { 52 | subject: "feat(foo): add a feature", 53 | body: "", 54 | hash, 55 | }, 56 | }, 57 | ]); 58 | 59 | assertEquals(parse("fix(foo,bar): add a feature", modules), [ 60 | { 61 | module: "foo", 62 | tag: "fix", 63 | version: "patch", 64 | commit: { 65 | subject: "fix(foo,bar): add a feature", 66 | body: "", 67 | hash, 68 | }, 69 | }, 70 | { 71 | module: "bar", 72 | tag: "fix", 73 | version: "patch", 74 | commit: { 75 | subject: "fix(foo,bar): add a feature", 76 | body: "", 77 | hash, 78 | }, 79 | }, 80 | ]); 81 | 82 | assertEquals(parse("fix(*): a bug", modules), [ 83 | { 84 | module: "foo", 85 | tag: "fix", 86 | version: "patch", 87 | commit: { 88 | subject: "fix(*): a bug", 89 | body: "", 90 | hash, 91 | }, 92 | }, 93 | { 94 | module: "bar", 95 | tag: "fix", 96 | version: "patch", 97 | commit: { 98 | subject: "fix(*): a bug", 99 | body: "", 100 | hash, 101 | }, 102 | }, 103 | ]); 104 | 105 | assertEquals(parse("BREAKING(foo): some breaking change", modules), [ 106 | { 107 | module: "foo", 108 | tag: "BREAKING", 109 | version: "major", 110 | commit: { 111 | subject: "BREAKING(foo): some breaking change", 112 | body: "", 113 | hash, 114 | }, 115 | }, 116 | ]); 117 | 118 | assertEquals(parse("perf(foo): update", modules), [ 119 | { 120 | module: "foo", 121 | tag: "perf", 122 | version: "patch", 123 | commit: { 124 | subject: "perf(foo): update", 125 | body: "", 126 | hash, 127 | }, 128 | }, 129 | ]); 130 | 131 | assertEquals(parse("docs(foo): update", modules), [ 132 | { 133 | module: "foo", 134 | tag: "docs", 135 | version: "patch", 136 | commit: { 137 | subject: "docs(foo): update", 138 | body: "", 139 | hash, 140 | }, 141 | }, 142 | ]); 143 | 144 | assertEquals(parse("style(foo): update", modules), [ 145 | { 146 | module: "foo", 147 | tag: "style", 148 | version: "patch", 149 | commit: { 150 | subject: "style(foo): update", 151 | body: "", 152 | hash, 153 | }, 154 | }, 155 | ]); 156 | 157 | assertEquals(parse("refactor(foo): update", modules), [ 158 | { 159 | module: "foo", 160 | tag: "refactor", 161 | version: "patch", 162 | commit: { 163 | subject: "refactor(foo): update", 164 | body: "", 165 | hash, 166 | }, 167 | }, 168 | ]); 169 | 170 | assertEquals(parse("test(foo): update", modules), [ 171 | { 172 | module: "foo", 173 | tag: "test", 174 | version: "patch", 175 | commit: { 176 | subject: "test(foo): update", 177 | body: "", 178 | hash, 179 | }, 180 | }, 181 | ]); 182 | 183 | assertEquals(parse("chore(foo): update", modules), [ 184 | { 185 | module: "foo", 186 | tag: "chore", 187 | version: "patch", 188 | commit: { 189 | subject: "chore(foo): update", 190 | body: "", 191 | hash, 192 | }, 193 | }, 194 | ]); 195 | 196 | assertEquals(parse("deprecation(foo): update", modules), [ 197 | { 198 | module: "foo", 199 | tag: "deprecation", 200 | version: "patch", 201 | commit: { 202 | subject: "deprecation(foo): update", 203 | body: "", 204 | hash, 205 | }, 206 | }, 207 | ]); 208 | 209 | assertEquals(parse("feat(foo/unstable): a new unstable feature", modules), [ 210 | { 211 | module: "foo", 212 | tag: "feat", 213 | version: "patch", 214 | commit: { 215 | subject: "feat(foo/unstable): a new unstable feature", 216 | body: "", 217 | hash, 218 | }, 219 | }, 220 | ]); 221 | 222 | assertEquals( 223 | parse("BREAKING(unstable/foo): break some unstable feature", modules), 224 | [ 225 | { 226 | module: "foo", 227 | tag: "BREAKING", 228 | version: "patch", 229 | commit: { 230 | subject: "BREAKING(unstable/foo): break some unstable feature", 231 | body: "", 232 | hash, 233 | }, 234 | }, 235 | ], 236 | ); 237 | }); 238 | 239 | Deno.test("checkModuleName()", () => { 240 | assertEquals( 241 | checkModuleName({ module: "foo", tag: "chore", commit: emptyCommit }, [ 242 | { name: "foo", version: "0.0.0", [pathProp]: "" }, 243 | ]), 244 | undefined, 245 | ); 246 | 247 | assertEquals( 248 | checkModuleName({ module: "foo", tag: "chore", commit: emptyCommit }, [ 249 | { name: "bar", version: "0.0.0", [pathProp]: "" }, 250 | ]), 251 | { 252 | type: "unknown_range_commit", 253 | commit: emptyCommit, 254 | reason: "Unknown module: foo.", 255 | }, 256 | ); 257 | 258 | assertEquals( 259 | checkModuleName({ module: "foo", tag: "feat", commit: emptyCommit }, [ 260 | { name: "bar", version: "0.0.0", [pathProp]: "" }, 261 | ]), 262 | { 263 | type: "unknown_range_commit", 264 | commit: emptyCommit, 265 | reason: "Unknown module: foo.", 266 | }, 267 | ); 268 | }); 269 | 270 | Deno.test("defaultParseCommitMessage() errors with invalid subject", () => { 271 | const modules: WorkspaceModule[] = [ 272 | { name: "foo", version: "0.0.0", [pathProp]: "" }, 273 | { name: "bar", version: "0.0.0", [pathProp]: "" }, 274 | ]; 275 | 276 | assertEquals(parse("random commit", modules), { 277 | type: "unknown_commit", 278 | commit: { 279 | subject: "random commit", 280 | body: "", 281 | hash, 282 | }, 283 | reason: "The commit message does not match the default pattern.", 284 | }); 285 | assertEquals(parse("fix: update", modules), { 286 | type: "missing_range", 287 | commit: { 288 | subject: "fix: update", 289 | body: "", 290 | hash, 291 | }, 292 | reason: "The commit message does not specify a module.", 293 | }); 294 | assertEquals(parse("chore: update", modules), { 295 | type: "skipped_commit", 296 | commit: { 297 | subject: "chore: update", 298 | body: "", 299 | hash, 300 | }, 301 | reason: "The commit message does not specify a module.", 302 | }); 303 | assertEquals(parse("hey(foo): update", modules), { 304 | type: "unknown_commit", 305 | commit: { 306 | subject: "hey(foo): update", 307 | body: "", 308 | hash, 309 | }, 310 | reason: "Unknown commit tag: hey.", 311 | }); 312 | }); 313 | 314 | const exampleVersionBumps = [ 315 | { 316 | module: "tools", 317 | tag: "feat", 318 | version: "minor", 319 | commit: { 320 | subject: 321 | "feat(tools,log,http,semver): check mod exports, export items consistently from mod.ts (#4229)", 322 | body: "", 323 | hash, 324 | }, 325 | }, 326 | { 327 | module: "log", 328 | tag: "feat", 329 | version: "minor", 330 | commit: { 331 | subject: 332 | "feat(tools,log,http,semver): check mod exports, export items consistently from mod.ts (#4229)", 333 | body: "", 334 | hash, 335 | }, 336 | }, 337 | { 338 | module: "http", 339 | tag: "feat", 340 | version: "minor", 341 | commit: { 342 | subject: 343 | "feat(tools,log,http,semver): check mod exports, export items consistently from mod.ts (#4229)", 344 | body: "", 345 | hash, 346 | }, 347 | }, 348 | { 349 | module: "semver", 350 | tag: "feat", 351 | version: "minor", 352 | commit: { 353 | subject: 354 | "feat(tools,log,http,semver): check mod exports, export items consistently from mod.ts (#4229)", 355 | body: "", 356 | hash, 357 | }, 358 | }, 359 | { 360 | module: "log", 361 | tag: "BREAKING", 362 | version: "major", 363 | commit: { 364 | subject: "BREAKING(log): remove string formatter (#4239)", 365 | body: "* BREAKING(log): remove `handlers.ts`\n" + 366 | "\n" + 367 | "* fix\n" + 368 | "\n" + 369 | "* BREAKING(log): remove string formatter", 370 | hash, 371 | }, 372 | }, 373 | { 374 | module: "streams", 375 | tag: "BREAKING", 376 | version: "major", 377 | commit: { 378 | subject: 379 | "BREAKING(streams): remove `readAll()`, `writeAll()` and `copy()` (#4238)", 380 | body: "", 381 | hash, 382 | }, 383 | }, 384 | { 385 | module: "streams", 386 | tag: "BREAKING", 387 | version: "major", 388 | commit: { 389 | subject: 390 | "feat(streams)!: remove `readAll()`, `writeAll()` and `copy()` (#4238)", 391 | body: "", 392 | hash, 393 | }, 394 | }, 395 | { 396 | module: "log", 397 | tag: "BREAKING", 398 | version: "major", 399 | commit: { 400 | subject: "BREAKING(log): single-export handler files (#4236)", 401 | body: "", 402 | hash, 403 | }, 404 | }, 405 | { 406 | module: "io", 407 | tag: "BREAKING", 408 | version: "major", 409 | commit: { 410 | subject: "BREAKING(io): remove `types.d.ts` (#4237)", 411 | body: "", 412 | hash, 413 | }, 414 | }, 415 | { 416 | module: "webgpu", 417 | tag: "refactor", 418 | version: "patch", 419 | commit: { 420 | subject: 421 | "refactor(webgpu): use internal `Deno.close()` for cleanup of WebGPU resources (#4231)", 422 | body: "", 423 | hash, 424 | }, 425 | }, 426 | { 427 | module: "collections", 428 | tag: "feat", 429 | version: "minor", 430 | commit: { 431 | subject: 432 | "feat(collections): pass `key` to `mapValues()` transformer (#4127)", 433 | body: "", 434 | hash, 435 | }, 436 | }, 437 | { 438 | module: "semver", 439 | tag: "deprecation", 440 | version: "patch", 441 | commit: { 442 | subject: 443 | "deprecation(semver): rename `eq()`, `neq()`, `lt()`, `lte()`, `gt()` and `gte()` (#4083)", 444 | body: "", 445 | hash, 446 | }, 447 | }, 448 | { 449 | module: "toml", 450 | tag: "docs", 451 | version: "patch", 452 | commit: { 453 | subject: "docs(toml): complete documentation (#4223)", 454 | body: "", 455 | hash, 456 | }, 457 | }, 458 | { 459 | module: "path", 460 | tag: "deprecation", 461 | version: "patch", 462 | commit: { 463 | subject: 464 | "deprecation(path): split off all constants into their own files and deprecate old names (#4153)", 465 | body: "", 466 | hash, 467 | }, 468 | }, 469 | { 470 | module: "msgpack", 471 | tag: "docs", 472 | version: "patch", 473 | commit: { 474 | subject: "docs(msgpack): complete documentation (#4220)", 475 | body: "", 476 | hash, 477 | }, 478 | }, 479 | { 480 | module: "media_types", 481 | tag: "docs", 482 | version: "patch", 483 | commit: { 484 | subject: "docs(media_types): complete documentation (#4219)", 485 | body: "", 486 | hash, 487 | }, 488 | }, 489 | { 490 | module: "log", 491 | tag: "fix", 492 | version: "patch", 493 | commit: { 494 | subject: "fix(log): make `flattenArgs()` private (#4214)", 495 | body: "", 496 | hash, 497 | }, 498 | }, 499 | { 500 | module: "streams", 501 | tag: "docs", 502 | version: "patch", 503 | commit: { 504 | subject: "docs(streams): remove `Deno.metrics()` use in example (#4217)", 505 | body: "", 506 | hash, 507 | }, 508 | }, 509 | { 510 | module: "log", 511 | tag: "refactor", 512 | version: "patch", 513 | commit: { 514 | subject: "refactor(log): tidy imports and exports (#4215)", 515 | body: "", 516 | hash, 517 | }, 518 | }, 519 | { 520 | module: "toml", 521 | tag: "test", 522 | version: "patch", 523 | commit: { 524 | subject: "test(toml): improve test coverage (#4211)", 525 | body: "", 526 | hash, 527 | }, 528 | }, 529 | { 530 | module: "console", 531 | tag: "refactor", 532 | version: "patch", 533 | commit: { 534 | subject: "refactor(console): rename `_rle` to `_run_length.ts` (#4212)", 535 | body: "", 536 | hash, 537 | }, 538 | }, 539 | { 540 | module: "http", 541 | tag: "docs", 542 | version: "patch", 543 | commit: { 544 | subject: "docs(http): complete documentation (#4209)", 545 | body: "", 546 | hash, 547 | }, 548 | }, 549 | { 550 | module: "fmt", 551 | tag: "fix", 552 | version: "patch", 553 | commit: { 554 | subject: "fix(fmt): correct `stripColor()` deprecation notice (#4208)", 555 | body: "", 556 | hash, 557 | }, 558 | }, 559 | { 560 | module: "flags", 561 | tag: "fix", 562 | version: "patch", 563 | commit: { 564 | subject: "fix(flags): correct deprecation notices (#4207)", 565 | body: "", 566 | hash, 567 | }, 568 | }, 569 | { 570 | module: "toml", 571 | tag: "fix", 572 | version: "patch", 573 | commit: { 574 | subject: 575 | "fix(toml): `parse()` duplicates the character next to reserved escape sequences (#4192)", 576 | body: "", 577 | hash, 578 | }, 579 | }, 580 | { 581 | module: "semver", 582 | tag: "refactor", 583 | version: "patch", 584 | commit: { 585 | subject: 586 | "refactor(semver): replace `parseComparator()` with comparator objects (#4204)", 587 | body: "", 588 | hash, 589 | }, 590 | }, 591 | { 592 | module: "expect", 593 | tag: "fix", 594 | version: "patch", 595 | commit: { 596 | subject: 597 | "fix(expect): fix the function signature of `toMatchObject()` (#4202)", 598 | body: "", 599 | hash, 600 | }, 601 | }, 602 | { 603 | module: "log", 604 | tag: "feat", 605 | version: "minor", 606 | commit: { 607 | subject: "feat(log): make handlers disposable (#4195)", 608 | body: "", 609 | hash, 610 | }, 611 | }, 612 | { 613 | module: "crypto", 614 | tag: "chore", 615 | version: "patch", 616 | commit: { 617 | subject: 618 | "chore(crypto): upgrade to `rust@1.75.0` and `wasmbuild@0.15.5` (#4193)", 619 | body: "", 620 | hash, 621 | }, 622 | }, 623 | { 624 | module: "using", 625 | tag: "refactor", 626 | version: "patch", 627 | commit: { 628 | subject: 629 | "refactor(using): use `using` keyword for Explicit Resource Management (#4143)", 630 | body: "", 631 | hash, 632 | }, 633 | }, 634 | { 635 | module: "semver", 636 | tag: "deprecation", 637 | version: "patch", 638 | commit: { 639 | subject: 640 | "deprecation(semver): deprecate `SemVerRange`, introduce `Range` (#4161)", 641 | body: "", 642 | hash, 643 | }, 644 | }, 645 | { 646 | module: "log", 647 | tag: "refactor", 648 | version: "patch", 649 | commit: { 650 | subject: "refactor(log): replace deprecated imports (#4188)", 651 | body: "", 652 | hash, 653 | }, 654 | }, 655 | { 656 | module: "semver", 657 | tag: "deprecation", 658 | version: "patch", 659 | commit: { 660 | subject: "deprecation(semver): deprecate `outside()` (#4185)", 661 | body: "", 662 | hash, 663 | }, 664 | }, 665 | { 666 | module: "io", 667 | tag: "feat", 668 | version: "minor", 669 | commit: { 670 | subject: "feat(io): un-deprecate `Buffer` (#4184)", 671 | body: "", 672 | hash, 673 | }, 674 | }, 675 | { 676 | module: "semver", 677 | tag: "BREAKING", 678 | version: "major", 679 | commit: { 680 | subject: "BREAKING(semver): remove `FormatStyle` (#4182)", 681 | body: "", 682 | hash, 683 | }, 684 | }, 685 | { 686 | module: "semver", 687 | tag: "BREAKING", 688 | version: "major", 689 | commit: { 690 | subject: "BREAKING(semver): remove `compareBuild()` (#4181)", 691 | body: "", 692 | hash, 693 | }, 694 | }, 695 | { 696 | module: "semver", 697 | tag: "BREAKING", 698 | version: "major", 699 | commit: { 700 | subject: "BREAKING(semver): remove `rsort()` (#4180)", 701 | body: "", 702 | hash, 703 | }, 704 | }, 705 | { 706 | module: "http", 707 | tag: "BREAKING", 708 | version: "major", 709 | commit: { 710 | subject: "BREAKING(http): remove `CookieMap` (#4179)", 711 | body: "", 712 | hash, 713 | }, 714 | }, 715 | ] as VersionBump[]; 716 | 717 | Deno.test("summarizeVersionBumpsByModule()", async (t) => { 718 | await assertSnapshot(t, summarizeVersionBumpsByModule(exampleVersionBumps)); 719 | }); 720 | 721 | Deno.test("maxVersion() returns the bigger version update from the given 2", () => { 722 | assertEquals(maxVersion("major", "minor"), "major"); 723 | assertEquals(maxVersion("minor", "major"), "major"); 724 | assertEquals(maxVersion("major", "patch"), "major"); 725 | assertEquals(maxVersion("patch", "major"), "major"); 726 | assertEquals(maxVersion("minor", "patch"), "minor"); 727 | assertEquals(maxVersion("patch", "minor"), "minor"); 728 | assertEquals(maxVersion("patch", "patch"), "patch"); 729 | }); 730 | 731 | Deno.test("tryGetDenoConfig()", async () => { 732 | const [_path, config] = await tryGetDenoConfig("."); 733 | assertEquals(config.name, denoJson.name); 734 | }); 735 | 736 | Deno.test("getWorkspaceModules()", async (t) => { 737 | const [_, modules] = await getWorkspaceModules("testdata/basic"); 738 | assertEquals(modules.length, 5); 739 | assertEquals(modules.map((m) => m.name), [ 740 | "@scope/foo", 741 | "@scope/bar", 742 | "@scope/baz", 743 | "@scope/qux", 744 | "@scope/quux", 745 | ]); 746 | await assertSnapshot(t, modules); 747 | }); 748 | 749 | Deno.test("getModule", async () => { 750 | const [_, modules] = await getWorkspaceModules("testdata/basic"); 751 | const mod = getModule("foo", modules); 752 | assertExists(mod); 753 | assertObjectMatch(mod, { 754 | name: "@scope/foo", 755 | version: "1.2.3", 756 | }); 757 | }); 758 | 759 | Deno.test("applyVersionBump() updates the version of the given module", async () => { 760 | const [denoJson, versionUpdate] = await applyVersionBump( 761 | { 762 | module: "foo", 763 | version: "minor", 764 | commits: [], 765 | }, 766 | { name: "@scope/foo", version: "1.0.0", [pathProp]: "foo/deno.json" }, 767 | { name: "@scope/foo", version: "1.0.0", [pathProp]: "foo/deno.json" }, 768 | `{ 769 | "imports": { 770 | "scope/foo": "jsr:@scope/foo@^1.0.0", 771 | "scope/bar": "jsr:@scope/bar@^1.0.0" 772 | } 773 | }`, 774 | true, 775 | ); 776 | assertEquals(versionUpdate.from, "1.0.0"); 777 | assertEquals(versionUpdate.to, "1.1.0"); 778 | assertEquals(versionUpdate.diff, "minor"); 779 | assertEquals( 780 | denoJson, 781 | `{ 782 | "imports": { 783 | "scope/foo": "jsr:@scope/foo@^1.1.0", 784 | "scope/bar": "jsr:@scope/bar@^1.0.0" 785 | } 786 | }`, 787 | ); 788 | }); 789 | 790 | Deno.test("applyVersionBump() consider major bump for 0.x version as minor bump", async () => { 791 | const [denoJson, updateResult] = await applyVersionBump( 792 | { 793 | module: "foo", 794 | version: "major", 795 | commits: [], 796 | }, 797 | { name: "@scope/foo", version: "0.0.0", [pathProp]: "foo/deno.jsonc" }, 798 | { name: "@scope/foo", version: "0.0.0", [pathProp]: "foo/deno.jsonc" }, 799 | `{ 800 | "imports": { 801 | "scope/foo": "jsr:@scope/foo@^0.0.0", 802 | "scope/bar": "jsr:@scope/bar@^1.0.0" 803 | } 804 | }`, 805 | true, 806 | ); 807 | assertEquals(updateResult.from, "0.0.0"); 808 | assertEquals(updateResult.to, "0.1.0"); 809 | assertEquals(updateResult.diff, "minor"); 810 | assertEquals( 811 | denoJson, 812 | `{ 813 | "imports": { 814 | "scope/foo": "jsr:@scope/foo@^0.1.0", 815 | "scope/bar": "jsr:@scope/bar@^1.0.0" 816 | } 817 | }`, 818 | ); 819 | }); 820 | 821 | Deno.test("applyVersionBump() consider minor bump for 0.x version as patch bump", async () => { 822 | const [denoJson, updateResult] = await applyVersionBump( 823 | { 824 | module: "foo", 825 | version: "minor", 826 | commits: [], 827 | }, 828 | { name: "@scope/foo", version: "0.1.0", [pathProp]: "foo/deno.jsonc" }, 829 | { name: "@scope/foo", version: "0.1.0", [pathProp]: "foo/deno.jsonc" }, 830 | `{ 831 | "imports": { 832 | "scope/foo": "jsr:@scope/foo@^0.1.0", 833 | "scope/bar": "jsr:@scope/bar@^1.0.0" 834 | } 835 | }`, 836 | true, 837 | ); 838 | assertEquals(updateResult.from, "0.1.0"); 839 | assertEquals(updateResult.to, "0.1.1"); 840 | assertEquals(updateResult.diff, "patch"); 841 | assertEquals( 842 | denoJson, 843 | `{ 844 | "imports": { 845 | "scope/foo": "jsr:@scope/foo@^0.1.1", 846 | "scope/bar": "jsr:@scope/bar@^1.0.0" 847 | } 848 | }`, 849 | ); 850 | }); 851 | 852 | Deno.test("applyVersionBump() consider any change to prerelease version as prerelease bump", async () => { 853 | const [denoJson, updateResult] = await applyVersionBump( 854 | { 855 | module: "foo", 856 | version: "minor", 857 | commits: [], 858 | }, 859 | { name: "@scope/foo", version: "1.0.0-rc.1", [pathProp]: "foo/deno.jsonc" }, 860 | { name: "@scope/foo", version: "1.0.0-rc.1", [pathProp]: "foo/deno.jsonc" }, 861 | `{ 862 | "imports": { 863 | "scope/foo": "jsr:@scope/foo@^1.0.0-rc.1", 864 | "scope/bar": "jsr:@scope/bar@^1.0.0" 865 | } 866 | }`, 867 | true, 868 | ); 869 | assertEquals(updateResult.from, "1.0.0-rc.1"); 870 | assertEquals(updateResult.to, "1.0.0-rc.2"); 871 | assertEquals(updateResult.diff, "prerelease"); 872 | assertEquals( 873 | denoJson, 874 | `{ 875 | "imports": { 876 | "scope/foo": "jsr:@scope/foo@^1.0.0-rc.2", 877 | "scope/bar": "jsr:@scope/bar@^1.0.0" 878 | } 879 | }`, 880 | ); 881 | }); 882 | 883 | Deno.test("applyVersionBump() respect manual version upgrade if the version between start and base is different", async () => { 884 | const [denoJson, updateResult] = await applyVersionBump( 885 | { 886 | module: "foo", 887 | version: "minor", // This version is ignored, instead manually given version is used for calculating actual version diff 888 | commits: [], 889 | }, 890 | { name: "@scope/foo", version: "1.0.0-rc.1", [pathProp]: "foo/deno.jsonc" }, 891 | { name: "@scope/foo", version: "0.224.0", [pathProp]: "foo/deno.jsonc" }, 892 | `{ 893 | "imports": { 894 | "scope/foo": "jsr:@scope/foo@^1.0.0-rc.1", 895 | "scope/bar": "jsr:@scope/bar@^1.0.0" 896 | } 897 | }`, 898 | true, 899 | ); 900 | assertEquals(updateResult.from, "0.224.0"); 901 | assertEquals(updateResult.to, "1.0.0-rc.1"); 902 | assertEquals(updateResult.diff, "prerelease"); 903 | assertEquals( 904 | denoJson, 905 | `{ 906 | "imports": { 907 | "scope/foo": "jsr:@scope/foo@^1.0.0-rc.1", 908 | "scope/bar": "jsr:@scope/bar@^1.0.0" 909 | } 910 | }`, 911 | ); 912 | }); 913 | 914 | Deno.test("applyVersionBump() respect manual version upgrade if the version between start and base is different (the case prerelease is removed)", async () => { 915 | const [denoJson, updateResult] = await applyVersionBump( 916 | { 917 | module: "foo", 918 | version: "patch", // This version is ignored, instead manually given version is used for calculating actual version diff 919 | commits: [], 920 | }, 921 | { name: "@scope/foo", version: "1.0.0", [pathProp]: "foo/deno.jsonc" }, 922 | { name: "@scope/foo", version: "1.0.0-rc.1", [pathProp]: "foo/deno.jsonc" }, 923 | `{ 924 | "imports": { 925 | "scope/foo": "jsr:@scope/foo@^1.0.0", 926 | "scope/bar": "jsr:@scope/bar@^1.0.0" 927 | } 928 | }`, 929 | true, 930 | ); 931 | assertEquals(updateResult.from, "1.0.0-rc.1"); 932 | assertEquals(updateResult.to, "1.0.0"); 933 | assertEquals(updateResult.diff, "major"); 934 | assertEquals( 935 | denoJson, 936 | `{ 937 | "imports": { 938 | "scope/foo": "jsr:@scope/foo@^1.0.0", 939 | "scope/bar": "jsr:@scope/bar@^1.0.0" 940 | } 941 | }`, 942 | ); 943 | }); 944 | 945 | Deno.test("applyVersionBump() works for new module (the case when oldModule is undefined)", async () => { 946 | const [denoJson, updateResult] = await applyVersionBump( 947 | { 948 | module: "foo", 949 | version: "patch", // <= this version is ignored, instead manually given version is used for calculating actual version diff 950 | commits: [], 951 | }, 952 | { name: "@scope/foo", version: "0.1.0", [pathProp]: "foo/deno.jsonc" }, 953 | undefined, 954 | `{ 955 | "imports": { 956 | "scope/foo": "jsr:@scope/foo@^0.1.0", 957 | "scope/bar": "jsr:@scope/bar@^1.0.0" 958 | } 959 | }`, 960 | true, 961 | ); 962 | assertEquals(updateResult.from, "0.0.0"); 963 | assertEquals(updateResult.to, "0.1.0"); 964 | assertEquals(updateResult.diff, "minor"); 965 | assertEquals( 966 | denoJson, 967 | `{ 968 | "imports": { 969 | "scope/foo": "jsr:@scope/foo@^0.1.0", 970 | "scope/bar": "jsr:@scope/bar@^1.0.0" 971 | } 972 | }`, 973 | ); 974 | }); 975 | 976 | async function createVersionUpdateResults( 977 | versionBumps: VersionBump[], 978 | modules: WorkspaceModule[], 979 | ) { 980 | const summaries = summarizeVersionBumpsByModule(versionBumps).filter(( 981 | { module }, 982 | ) => getModule(module, modules) !== undefined); 983 | const diagnostics = versionBumps.map((versionBump) => 984 | checkModuleName(versionBump, modules) 985 | ).filter(Boolean) as Diagnostic[]; 986 | const updates = []; 987 | for (const summary of summaries) { 988 | const [_denoJson, versionUpdate] = await applyVersionBump( 989 | summary, 990 | getModule(summary.module, modules)!, 991 | getModule(summary.module, modules)!, 992 | "", 993 | true, 994 | ); 995 | updates.push(versionUpdate); 996 | } 997 | return [updates, diagnostics] as const; 998 | } 999 | 1000 | Deno.test("createReleaseNote()", async (t) => { 1001 | const [_, modules] = await getWorkspaceModules("testdata/std_mock"); 1002 | const [updates, _diagnostics] = await createVersionUpdateResults( 1003 | exampleVersionBumps, 1004 | modules, 1005 | ); 1006 | await assertSnapshot(t, createReleaseNote(updates, modules, new Date(0))); 1007 | }); 1008 | 1009 | Deno.test("createPrBody()", async (t) => { 1010 | const [_, modules] = await getWorkspaceModules("testdata/std_mock"); 1011 | const [updates, diagnostics] = await createVersionUpdateResults( 1012 | exampleVersionBumps, 1013 | modules, 1014 | ); 1015 | await assertSnapshot( 1016 | t, 1017 | createPrBody( 1018 | updates, 1019 | diagnostics, 1020 | "denoland/deno_std", 1021 | "release-1970-01-01-00-00-00", 1022 | ), 1023 | ); 1024 | }); 1025 | 1026 | Deno.test("createReleaseBranchName()", () => { 1027 | const date = new Date(0); 1028 | assertEquals( 1029 | createReleaseBranchName(date), 1030 | "release-1970-01-01-00-00-00", 1031 | ); 1032 | }); 1033 | 1034 | Deno.test("createReleaseTitle()", () => { 1035 | const date = new Date(0); 1036 | assertEquals(createReleaseTitle(date), "1970.01.01"); 1037 | }); 1038 | --------------------------------------------------------------------------------