├── ._travis.yml ├── .github └── workflows │ ├── ci.yaml │ └── codeql-analysis.yml ├── .gitignore ├── .releaserc.js ├── .yarn ├── plugins │ └── @yarnpkg │ │ └── plugin-workspace-tools.cjs └── releases │ └── yarn-4.0.0-rc.45.cjs ├── .yarnrc.yml ├── LICENSE ├── README.md ├── jest.config.js ├── package.json ├── packages ├── common │ ├── .eslintrc.cjs │ ├── CHANGELOG.md │ ├── README.md │ ├── jest.config.cjs │ ├── package.json │ ├── src │ │ ├── main │ │ │ └── ts │ │ │ │ ├── flags.ts │ │ │ │ ├── ifaces.ts │ │ │ │ ├── index.ts │ │ │ │ └── tpl.ts │ │ └── test │ │ │ └── ts │ │ │ └── unit │ │ │ ├── flags.ts │ │ │ ├── index.ts │ │ │ └── tpl.ts │ ├── tsconfig.es6.json │ ├── tsconfig.esnext.json │ ├── tsconfig.json │ ├── tsconfig.test.json │ └── typedoc.json ├── config-monorepo │ ├── CHANGELOG.md │ ├── README.md │ ├── jest.config.js │ ├── package.json │ └── src │ │ ├── main │ │ └── js │ │ │ └── index.js │ │ └── test │ │ └── js │ │ └── index.js ├── config │ ├── CHANGELOG.md │ ├── README.md │ ├── jest.config.js │ ├── package.json │ └── src │ │ ├── main │ │ └── js │ │ │ └── index.js │ │ └── test │ │ └── js │ │ └── index.js ├── git-sync │ ├── CHANGELOG.md │ ├── README.md │ ├── jest.config.cjs │ ├── package.json │ └── src │ │ ├── main │ │ └── js │ │ │ ├── get-git-auth-url.js │ │ │ └── index.js │ │ └── test │ │ └── js │ │ └── index.js ├── git-utils │ ├── .eslintrc.cjs │ ├── CHANGELOG.md │ ├── README.md │ ├── jest.config.cjs │ ├── package.json │ ├── src │ │ ├── main │ │ │ └── ts │ │ │ │ ├── git │ │ │ │ ├── add.ts │ │ │ │ ├── branch.ts │ │ │ │ ├── checkout.ts │ │ │ │ ├── commit.ts │ │ │ │ ├── config.ts │ │ │ │ ├── etc.ts │ │ │ │ ├── exec.ts │ │ │ │ ├── fetch.ts │ │ │ │ ├── index.ts │ │ │ │ ├── init.ts │ │ │ │ ├── push.ts │ │ │ │ ├── rebase.ts │ │ │ │ ├── remote.ts │ │ │ │ ├── tag.ts │ │ │ │ └── user.ts │ │ │ │ ├── ifaces.ts │ │ │ │ ├── index.ts │ │ │ │ └── misc.ts │ │ └── test │ │ │ ├── fixtures │ │ │ ├── basicPackage │ │ │ │ ├── inner │ │ │ │ │ └── file.txt │ │ │ │ └── package.json │ │ │ └── foo │ │ │ │ ├── bar │ │ │ │ └── foobar.txt │ │ │ │ ├── baz │ │ │ │ └── foobaz.txt │ │ │ │ └── foo │ │ │ │ └── foofoo.txt │ │ │ └── ts │ │ │ ├── it │ │ │ └── git.ts │ │ │ └── unit │ │ │ ├── git.ts │ │ │ └── index.ts │ ├── tsconfig.es6.json │ ├── tsconfig.esnext.json │ ├── tsconfig.json │ ├── tsconfig.test.json │ └── typedoc.json ├── infra │ ├── CHANGELOG.md │ ├── README.md │ ├── index.js │ ├── jest.config.json │ ├── package.json │ └── tsconfig.compiler.json ├── metabranch │ ├── .eslintrc.cjs │ ├── CHANGELOG.md │ ├── README.md │ ├── jest.config.cjs │ ├── package.json │ ├── src │ │ ├── main │ │ │ ├── exports │ │ │ │ ├── es5.cjs │ │ │ │ └── es6.mjs │ │ │ └── ts │ │ │ │ ├── actions.ts │ │ │ │ ├── index.ts │ │ │ │ ├── interface.ts │ │ │ │ ├── options.ts │ │ │ │ └── plugin.ts │ │ └── test │ │ │ ├── fixtures │ │ │ ├── basicPackage │ │ │ │ ├── inner │ │ │ │ │ └── file.txt │ │ │ │ └── package.json │ │ │ └── foo │ │ │ │ ├── bar │ │ │ │ └── foobar.txt │ │ │ │ ├── baz │ │ │ │ └── foobaz.txt │ │ │ │ └── foo │ │ │ │ └── foofoo.txt │ │ │ └── ts │ │ │ ├── actions.ts │ │ │ ├── index.ts │ │ │ └── plugin.ts │ ├── tsconfig.es5.json │ ├── tsconfig.es6.json │ ├── tsconfig.esnext.json │ ├── tsconfig.json │ ├── tsconfig.test.json │ └── typedoc.json ├── plugin-actions │ ├── .eslintrc.js │ ├── CHANGELOG.md │ ├── README.md │ ├── jest.config.js │ ├── package.json │ ├── src │ │ ├── main │ │ │ └── ts │ │ │ │ ├── index.ts │ │ │ │ ├── interface.ts │ │ │ │ └── providers │ │ │ │ └── metabranch.ts │ │ └── test │ │ │ ├── js │ │ │ └── index.js │ │ │ └── ts │ │ │ └── index.ts │ ├── tsconfig.es5.json │ ├── tsconfig.es6.json │ ├── tsconfig.json │ ├── tsconfig.test.json │ └── typedoc.json ├── plugin-creator │ ├── .eslintrc.cjs │ ├── CHANGELOG.md │ ├── README.md │ ├── jest.config.mjs │ ├── package.json │ ├── src │ │ ├── main │ │ │ ├── exports │ │ │ │ ├── es5.cjs │ │ │ │ └── es6.mjs │ │ │ └── ts │ │ │ │ ├── index.ts │ │ │ │ └── interface.ts │ │ └── test │ │ │ ├── cjs │ │ │ └── index.cjs │ │ │ ├── fixtures │ │ │ └── yarnWorkspaces │ │ │ │ ├── package.json │ │ │ │ └── packages │ │ │ │ ├── a │ │ │ │ └── package.json │ │ │ │ ├── b │ │ │ │ └── package.json │ │ │ │ ├── c │ │ │ │ ├── .releaserc.json │ │ │ │ └── package.json │ │ │ │ └── d │ │ │ │ └── package.json │ │ │ ├── mjs │ │ │ └── index.mjs │ │ │ └── ts │ │ │ ├── index.ts │ │ │ └── integration.ts │ ├── tsconfig.es5.json │ ├── tsconfig.es6.json │ ├── tsconfig.esnext.json │ ├── tsconfig.json │ ├── tsconfig.test.json │ └── typedoc.json ├── preset │ ├── CHANGELOG.md │ ├── README.md │ ├── jest.config.js │ ├── package.json │ └── src │ │ └── test │ │ └── js │ │ └── index.js ├── testing-suite │ ├── .eslintrc.cjs │ ├── CHANGELOG.md │ ├── README.md │ ├── jest.config.cjs │ ├── package.json │ ├── src │ │ ├── main │ │ │ └── ts │ │ │ │ ├── file.ts │ │ │ │ ├── git.ts │ │ │ │ └── index.ts │ │ └── test │ │ │ ├── fixtures │ │ │ ├── basicPackage │ │ │ │ └── package.json │ │ │ └── foo │ │ │ │ ├── bar │ │ │ │ └── foobar.txt │ │ │ │ ├── baz │ │ │ │ └── foobaz.txt │ │ │ │ └── foo │ │ │ │ └── foofoo.txt │ │ │ └── ts │ │ │ ├── it │ │ │ └── git.ts │ │ │ └── unit │ │ │ ├── file.ts │ │ │ ├── git.ts │ │ │ └── index.ts │ ├── tsconfig.es5.json │ ├── tsconfig.es6.json │ ├── tsconfig.esnext.json │ ├── tsconfig.json │ ├── tsconfig.test.json │ └── typedoc.json └── toolkit │ ├── CHANGELOG.md │ ├── README.md │ ├── msr.js │ ├── package.json │ └── semrel.js ├── renovate.json ├── scripts ├── js │ └── coverage-merge.js └── sh │ ├── patch-pkg-main.sh │ └── update.sh ├── tsconfig.esnext.json ├── tsconfig.json └── yarn.lock /._travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 14 3 | install: skip 4 | cache: yarn 5 | 6 | os: linux 7 | dist: focal 8 | 9 | jobs: 10 | fast_finish: true 11 | include: 12 | - stage: verify 13 | if: branch != master AND type != pull_request 14 | install: 15 | - yarn 16 | - yarn bootstrap 17 | script: 18 | - yarn build 19 | - if [ "$CI_TEST" != "false" ]; then 20 | yarn test; 21 | fi 22 | - &build 23 | if: branch = master 24 | stage: build 25 | install: 26 | - yarn 27 | - yarn bootstrap 28 | script: 29 | - yarn build 30 | - yarn uglify 31 | # https://docs.travis-ci.com/user/using-workspaces/ 32 | workspaces: 33 | create: 34 | name: linux-shared 35 | paths: 36 | - node_modules 37 | - packages 38 | - <<: *build 39 | if: branch = master AND env(CI_WIN_BUILD) = true AND type = pull_request 40 | os: windows 41 | script: yarn build 42 | # https://travis-ci.community/t/timeout-after-build-finished-and-succeeded/1336/2 43 | env: YARN_GPG=no 44 | workspaces: 45 | create: 46 | name: win-shared 47 | paths: 48 | - node_modules 49 | - packages/**/target 50 | 51 | - &test 52 | if: branch = master AND type = pull_request AND env(CI_TEST) != false 53 | stage: test 54 | script: yarn test 55 | workspaces: 56 | use: linux-shared 57 | - <<: *test 58 | node_js: 12 59 | - <<: *test 60 | if: branch = master AND type != pull_request AND env(CI_TEST) != false 61 | before_script: 62 | - if [ "$CC_TEST_REPORTER_ID" != "" ]; then 63 | curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter; 64 | chmod +x ./cc-test-reporter; 65 | ./cc-test-reporter before-build; 66 | fi 67 | script: 68 | - if [ "$CC_TEST_REPORTER_ID" != "" ]; then 69 | yarn test:report; 70 | else 71 | yarn test; 72 | fi 73 | after_script: 74 | - if [ "$CC_TEST_REPORTER_ID" != "" ]; then 75 | ./cc-test-reporter format-coverage -t lcov ./coverage/lcov.info; 76 | ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT; 77 | fi 78 | - <<: *test 79 | if: branch = master AND type = pull_request AND env(CI_WIN_BUILD) = true AND env(CI_TEST) != false 80 | os: windows 81 | env: YARN_GPG=no 82 | install: yarn bootstrap 83 | workspaces: 84 | use: win-shared 85 | - <<: *test 86 | if: branch = master AND type = pull_request AND env(CI_WIN_BUILD) = true AND env(CI_TEST) != false 87 | os: windows 88 | node_js: 12 89 | install: yarn bootstrap 90 | env: YARN_GPG=no 91 | workspaces: 92 | use: win-shared 93 | 94 | - stage: release 95 | if: branch = master AND type != pull_request AND env(CI_RELEASE) = true 96 | workspaces: 97 | use: linux-shared 98 | script: yarn release 99 | # script: echo 'Deploy step is disabled' && exit 0 100 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | # This is a Github Workflow that runs tests on any push or pull request. 2 | # If the tests pass and this is a push to the master branch it also runs Semantic Release. 3 | name: CI 4 | on: [push, pull_request] 5 | jobs: 6 | init: 7 | name: init 8 | runs-on: ubuntu-22.04 9 | outputs: 10 | skip: ${{ steps.ci-skip-step.outputs.ci-skip }} 11 | skip-not: ${{ steps.ci-skip-step.outputs.ci-skip-not }} 12 | steps: 13 | - uses: actions/checkout@v3 14 | 15 | - id: ci-skip-step 16 | uses: mstachniuk/ci-skip@v1 17 | 18 | lint: 19 | name: lint 20 | needs: init 21 | runs-on: ubuntu-22.04 22 | steps: 23 | - uses: actions/checkout@v3 24 | - uses: actions/setup-node@v3 25 | with: 26 | node-version: 16 27 | - name: Install deps 28 | run: yarn 29 | - name: Lint 30 | run: yarn lint 31 | 32 | build: 33 | name: build 34 | needs: lint 35 | if: ${{ needs.init.outputs.skip == 'false' }} 36 | runs-on: ubuntu-22.04 37 | steps: 38 | - name: Checkout 39 | uses: actions/checkout@v3 40 | 41 | - uses: actions/setup-node@v3 42 | with: 43 | node-version: 16 44 | - name: Install deps 45 | run: yarn 46 | 47 | - name: Build 48 | run: yarn build 49 | 50 | - name: Save target (artifact) 51 | uses: actions/upload-artifact@v3 52 | with: 53 | name: target 54 | retention-days: 1 55 | # If a wildcard pattern is used, the path hierarchy will be preserved after the first wildcard pattern. 56 | # https://github.com/actions/upload-artifact#upload-using-multiple-paths-and-exclusions 57 | path: | 58 | !packages/*/node_modules 59 | !packages/*/src 60 | packages 61 | renovate.json 62 | 63 | test_push: 64 | needs: build 65 | if: github.event_name == 'push' 66 | runs-on: ubuntu-22.04 67 | steps: 68 | - name: Checkout 69 | uses: actions/checkout@v3 70 | 71 | - name: Restore target 72 | uses: actions/download-artifact@v3 73 | with: 74 | name: target 75 | - uses: actions/setup-node@v3 76 | with: 77 | node-version: 16 78 | - name: Install deps 79 | run: | 80 | sudo apt-get install moreutils 81 | yarn 82 | 83 | - name: Unit test only 84 | run: yarn test:unit 85 | 86 | - name: Push coverage 87 | if: github.ref == 'refs/heads/master' 88 | uses: actions/upload-artifact@v3 89 | with: 90 | name: target 91 | retention-days: 1 92 | path: coverage 93 | 94 | test_pr: 95 | if: github.event_name == 'pull_request' 96 | needs: build 97 | strategy: 98 | matrix: 99 | os: [ ubuntu-22.04 ] 100 | node-version: [ 14, 16 ] 101 | name: Test (Node v${{ matrix.node-version }}, OS ${{ matrix.os }}) 102 | runs-on: ${{ matrix.os }} 103 | steps: 104 | - name: Checkout 105 | uses: actions/checkout@v3 106 | 107 | - name: Restore target 108 | uses: actions/download-artifact@v3 109 | with: 110 | name: target 111 | - uses: actions/setup-node@v3 112 | with: 113 | node-version: ${{ matrix.node-version }} 114 | - name: Install deps 115 | run: | 116 | sudo apt-get install moreutils 117 | yarn 118 | - name: Unit test only 119 | if: matrix.node-version != '16' || matrix.os != 'ubuntu-22.04' 120 | run: yarn test:unit 121 | - name: Full test 122 | if: matrix.node-version == '16' && matrix.os == 'ubuntu-22.04' 123 | run: yarn test 124 | 125 | release: 126 | name: Release 127 | # https://github.community/t/trigger-job-on-tag-push-only/18076 128 | if: github.event_name == 'push' && github.ref == 'refs/heads/master' 129 | needs: test_push 130 | runs-on: ubuntu-22.04 131 | steps: 132 | - name: Checkout 133 | uses: actions/checkout@v3 134 | with: 135 | fetch-depth: 0 136 | 137 | - name: Restore target 138 | uses: actions/download-artifact@v3 139 | with: 140 | name: target 141 | - uses: actions/setup-node@v3 142 | with: 143 | node-version: 16 144 | 145 | - name: Codeclimate 146 | uses: paambaati/codeclimate-action@v3.2.0 147 | env: 148 | CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }} 149 | with: 150 | coverageLocations: | 151 | ${{github.workspace}}/coverage/*.lcov:lcov 152 | 153 | - name: Install deps 154 | # https://github.com/npm/cli/issues/4600 155 | run: | 156 | npm -v 157 | yarn -v 158 | yarn 159 | 160 | - name: Release 161 | env: 162 | GH_TOKEN: ${{ secrets.GH_TOKEN }} 163 | GH_USER: 'qiwibot' 164 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 165 | GIT_AUTHOR_EMAIL: 'opensource@qiwi.com' 166 | GIT_COMMITTER_EMAIL: 'opensource@qiwi.com' 167 | GIT_AUTHOR_NAME: '@qiwibot' 168 | GIT_COMMITTER_NAME: '@qiwibot' 169 | YARN_ENABLE_IMMUTABLE_INSTALLS: false 170 | run: yarn release 171 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # ******** NOTE ******** 12 | 13 | name: "CodeQL" 14 | 15 | on: 16 | push: 17 | branches: [ master ] 18 | pull_request: 19 | # The branches below must be a subset of the branches above 20 | branches: [ master ] 21 | schedule: 22 | - cron: '23 19 * * 5' 23 | 24 | jobs: 25 | analyze: 26 | name: Analyze 27 | runs-on: ubuntu-latest 28 | 29 | strategy: 30 | fail-fast: false 31 | matrix: 32 | language: [ 'javascript' ] 33 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 34 | # Learn more... 35 | # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection 36 | 37 | steps: 38 | - name: Checkout repository 39 | uses: actions/checkout@v3 40 | 41 | # Initializes the CodeQL tools for scanning. 42 | - name: Initialize CodeQL 43 | uses: github/codeql-action/init@v2 44 | with: 45 | languages: ${{ matrix.language }} 46 | # If you wish to specify custom queries, you can do so here or in a config file. 47 | # By default, queries listed here will override any specified in a config file. 48 | # Prefix the list here with "+" to use these queries and those in the config file. 49 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 50 | 51 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 52 | # If this step fails, then you should remove it and run the build manually (see below) 53 | - name: Autobuild 54 | uses: github/codeql-action/autobuild@v2 55 | 56 | # ℹ️ Command-line programs to run using the OS shell. 57 | # 📚 https://git.io/JvXDl 58 | 59 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 60 | # and modify them (or add more) to build your code if your project 61 | # uses a compiled language 62 | 63 | #- run: | 64 | # make bootstrap 65 | # make release 66 | 67 | - name: Perform CodeQL Analysis 68 | uses: github/codeql-action/analyze@v2 69 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Coverage 19 | flow-coverage 20 | lib-cov 21 | coverage 22 | .nyc_output 23 | 24 | # Bundle 25 | build 26 | dist 27 | lib 28 | target 29 | 30 | # Docs 31 | docs 32 | doc 33 | 34 | # Deps 35 | node_modules/ 36 | jspm_packages/ 37 | flow-typed/npm/ 38 | 39 | .npm 40 | .eslintcache 41 | .node_repl_history 42 | .env 43 | 44 | # IDE 45 | .idea 46 | *.iml 47 | 48 | # Libdefs 49 | flow-typed 50 | typings 51 | 52 | # Temp 53 | temp 54 | *.tmp 55 | 56 | # Typescript 57 | *.tsbuildinfo 58 | .tsbuildinfo 59 | buildcache 60 | .buildcache 61 | .rts2_cache_cjs 62 | .rts2_cache_es 63 | .rts2_cache_umd 64 | 65 | # Yarn berry 66 | .pnp.* 67 | .yarn/* 68 | !.yarn/patches 69 | !.yarn/plugins 70 | !.yarn/releases 71 | !.yarn/sdks 72 | !.yarn/versions 73 | -------------------------------------------------------------------------------- /.releaserc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | cmd: 'yarn', 3 | changelog: 'changelog' 4 | } 5 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | enableTelemetry: false 2 | 3 | networkConcurrency: 256 4 | 5 | nmSelfReferences: false 6 | 7 | nodeLinker: node-modules 8 | 9 | plugins: 10 | - path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs 11 | spec: "@yarnpkg/plugin-workspace-tools" 12 | 13 | yarnPath: .yarn/releases/yarn-4.0.0-rc.45.cjs 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, 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, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @qiwi/semrel 2 | [![CI](https://github.com/qiwi/semantic-release-toolkit/actions/workflows/ci.yaml/badge.svg)](https://github.com/qiwi/semantic-release-toolkit/actions/workflows/ci.yaml) 3 | [![Maintainability](https://api.codeclimate.com/v1/badges/202e9bc2e0d5ed528ed0/maintainability)](https://codeclimate.com/github/qiwi/semantic-release-toolkit/maintainability) 4 | [![Test Coverage](https://api.codeclimate.com/v1/badges/202e9bc2e0d5ed528ed0/test_coverage)](https://codeclimate.com/github/qiwi/semantic-release-toolkit/test_coverage) 5 | 6 | [Semantic-release](https://github.com/semantic-release/semantic-release) tools, plugins and configs for QIWI OSS projects 7 | 8 | ## Contents 9 | | Package | Description | Latest | 10 | |----------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------| 11 | | [@qiwi/git-utils](./packages/git-utils) | Utils and helpers to interact with git | [![npm](https://img.shields.io/npm/v/@qiwi/git-utils/latest.svg?label=&color=09e)](https://www.npmjs.com/package/@qiwi/git-utils) | 12 | | [@qiwi/semrel-toolkit](./packages/toolkit) | All-in-one utility to run [semantic-release](https://github.com/semantic-release/semantic-release) and [multi-semantic-release](https://github.com/qiwi/multi-semantic-release) tasks | [![npm](https://img.shields.io/npm/v/@qiwi/semrel-toolkit/latest.svg?label=&color=09e)](https://www.npmjs.com/package/@qiwi/semrel-toolkit) | 13 | | [@qiwi/semrel-config](./packages/config) | Basic config to deploy a single github-hosted npm-package | [![npm](https://img.shields.io/npm/v/@qiwi/semrel-config/latest.svg?label=&color=09e)](https://www.npmjs.com/package/@qiwi/semrel-config) | 14 | | [@qiwi/semrel-config-monorepo](./packages/config) | Config to release github-hosted monorepos | [![npm](https://img.shields.io/npm/v/@qiwi/semrel-config-monorepo/latest.svg?label=&color=09e)](https://www.npmjs.com/package/@qiwi/semrel-config-monorepo) | 15 | | [@qiwi/semrel-preset](./packages/preset) | Semrel plugin preset | [![npm](https://img.shields.io/npm/v/@qiwi/semrel-preset/latest.svg?label=&color=09e)](https://www.npmjs.com/package/@qiwi/semrel-preset) | 16 | | [@qiwi/semrel-plugin-creator](./packages/plugin-creator) | Semrel plugin factory | [![npm](https://img.shields.io/npm/v/@qiwi/semrel-plugin-creator/latest.svg?label=&color=09e)](https://www.npmjs.com/package/@qiwi/semrel-plugin-creator) | 17 | | [@qiwi/semrel-infra](./packages/infra) | Infra package: common assets, deps, etc | [![npm](https://img.shields.io/npm/v/@qiwi/semrel-infra/latest.svg?label=&color=09e)](https://www.npmjs.com/package/@qiwi/semrel-infra) | 18 | | [@qiwi/semrel-testing-suite](./packages/testing-suite) | Testing helpers to verify release flow
**experimental** | [![npm](https://img.shields.io/npm/v/@qiwi/semrel-testing-suite/latest.svg?label=&color=fc3)](https://www.npmjs.com/package/@qiwi/semrel-testing-suite) | 19 | | [@qiwi/semrel-metabranch](./packages/metabranch) | Plugin for two-way data sync with remote branch
**experimental** | [![npm](https://img.shields.io/npm/v/@qiwi/semrel-metabranch/latest.svg?label=&color=fc3)](https://www.npmjs.com/package/@qiwi/semrel-metabranch) | 20 | 21 | ### Coming ~~soon~~ someday 22 | | Package | Description | 23 | |-----------------------|-----------------------------------------------------------------------------------------------| 24 | | @qiwi/semrel-actions | Configurable custom actions/side-effects provider | 25 | | @qiwi/msr | **[multi-semantic-release](https://github.com/qiwi/multi-semantic-release)** reforged with TS | 26 | | @qiwi/semrel-monorepo | Represents **msr** as a regular plugin | 27 | 28 | ## License 29 | [MIT](./LICENSE) 30 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const PROJECT = process.env.JEST_PROJECT 2 | const projects = [ 3 | 'common', 4 | 'config', 5 | 'config-monorepo', 6 | 'git-utils', 7 | 'metabranch', 8 | 'preset', 9 | 'plugin-creator', 10 | 'plugin-actions', 11 | 'testing-suite' 12 | ] 13 | 14 | module.exports = { 15 | collectCoverage: true, 16 | collectCoverageFrom: [ 17 | '/**/src/main/**/*.(j|t)s' 18 | ], 19 | testFailureExitCode: 1, 20 | projects: (PROJECT ? [PROJECT] : projects).map(name => `/packages/${name}/`), 21 | } 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@qiwi/semrel-toolkit-monorepo", 3 | "version": "0.0.0", 4 | "description": "Semantic release tools, plugins and configs for QIWI OSS projects", 5 | "workspaces": [ 6 | "packages/*" 7 | ], 8 | "private": true, 9 | "scripts": { 10 | "clean": "yarn workspaces foreach -p run clean", 11 | "lint": "yarn workspaces foreach -tp run lint", 12 | "lint:fix": "yarn workspaces foreach -tp run lint:fix", 13 | "test": "./node_modules/.bin/concurrently yarn:lint yarn:test:unit yarn:test:depcheck yarn:test:depaudit", 14 | "jest:pre": "sh ./scripts/sh/patch-pkg-main.sh true", 15 | "jest:post": "sh ./scripts/sh/patch-pkg-main.sh", 16 | "test:unit": "yarn jest:pre && NODE_OPTIONS=--experimental-vm-modules ./node_modules/.bin/jest --detectOpenHandles --forceExit --runInBand && yarn jest:post", 17 | "test:report": "yarn test && yarn coveralls:push", 18 | "test:concurrent": "yarn workspaces foreach -tp run && yarn coverage:merge", 19 | "test:depcheck": "yarn workspaces foreach -tp run test:depcheck", 20 | "test:depaudit": "yarn npm audit -R -A --severity moderate --environment production || echo 'yarn audit failed :(' && exit 0", 21 | "test:depauditfix": "npm_config_yes=true npx yarn-audit-fix --audit-level=moderate", 22 | "prebuild": "tsc -b", 23 | "build": "yarn workspaces foreach -tp run build", 24 | "coverage:merge": "node scripts/js/coverage-merge.js", 25 | "coveralls:push": "cat ./coverage/lcov.info | npm_config_yes=true npx coveralls || echo 'coveralls push failed :(' && exit 0", 26 | "release": "npm_config_yes=true npx zx-bulk-release", 27 | "updeps": "npm_config_yes=true npx npm-upgrade-monorepo", 28 | "postupdate": "yarn && yarn build && yarn test" 29 | }, 30 | "devDependencies": { 31 | "find-git-root": "^1.0.4" 32 | }, 33 | "resolutions": { 34 | "npm/chalk": "^4.1.2" 35 | }, 36 | "repository": { 37 | "type": "git", 38 | "url": "git+https://github.com/qiwi/semantic-release-toolkit.git" 39 | }, 40 | "packageManager": "yarn@4.0.0-rc.45" 41 | } 42 | -------------------------------------------------------------------------------- /packages/common/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | 'eslint-config-qiwi', 4 | 'prettier', 5 | ], 6 | overrides: [ 7 | { 8 | files: ['./src/test/**/*.ts'], 9 | rules: { 10 | 'unicorn/consistent-function-scoping': 'off', 11 | 'sonarjs/no-duplicate-string': 'off' 12 | } 13 | } 14 | ] 15 | }; 16 | -------------------------------------------------------------------------------- /packages/common/README.md: -------------------------------------------------------------------------------- 1 | # @qiwi/semrel-common 2 | Common semrel utils: config reader, git-client, etc. 3 | 4 | ## Install 5 | ```shell script 6 | yarn add @qiwi/semrel-common 7 | ``` 8 | 9 | ## Usage 10 | ```typescript 11 | import { gitTags } from '@qiwi/semrel-common' 12 | 13 | const tags = await gitTags({ cwd: '/foo/bar/baz', branch: 'master' }) 14 | ``` 15 | -------------------------------------------------------------------------------- /packages/common/jest.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = {...require('../infra/jest.config.json')} 2 | -------------------------------------------------------------------------------- /packages/common/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@qiwi/semrel-common", 3 | "version": "3.4.2", 4 | "publishConfig": { 5 | "access": "public" 6 | }, 7 | "description": "Semrel common utils", 8 | "keywords": [], 9 | "type": "module", 10 | "module": "./target/es6/index.js", 11 | "main": "./target/es6/index.js", 12 | "exports": "./target/es6/index.js", 13 | "source": "target/ts/index.ts", 14 | "types": "./target/es6/index.d.ts", 15 | "files": [ 16 | "README.md", 17 | "CHANGELOG.md", 18 | "target", 19 | "typings", 20 | "flow-typed" 21 | ], 22 | "scripts": { 23 | "clean": "rimraf target typings flow-typed buildcache coverage docs", 24 | "lint": "eslint 'src/**/*.ts'", 25 | "lint:fix": "yarn lint --fix", 26 | "format": "prettier --write 'src/**/*.ts'", 27 | "test": "concurrently yarn:lint yarn:test:unit yarn:test:depcheck", 28 | "test:unit": "NODE_OPTIONS=--experimental-vm-modules jest --detectOpenHandles --forceExit --runInBand", 29 | "test:depcheck": "npm_config_yes=true npx -p depcheck -p @babel/parser@7.16.4 depcheck --ignores='@jest/globals,@types/*,tslib,eslint-*,prettier-*,@qiwi/semrel-infra,@qiwi/semrel-common,semantic-release' --ignore-patterns='typings,flow-typed/*'", 30 | "build": "concurrently yarn:build:esnext yarn:build:es6 yarn:build:ts yarn:build:libdef yarn:docs && yarn build:esmfix", 31 | "build:esnext": "mkdirp target/esnext && tsc -p tsconfig.esnext.json", 32 | "build:es6": "mkdirp target/es6 && tsc -p tsconfig.es6.json", 33 | "build:ts": "cpy ./ ../../../target/ts/ --dot --cwd=./src/main/ts/", 34 | "build:libdef": "libdefkit --tsconfig=tsconfig.esnext.json --tsconfig=tsconfig.es6.json --entry=@qiwi/semrel-common/target/es6", 35 | "build:esmfix": "yarn tsc-esm-fix --target=target/es6 --target=target/esnext --ext=.js", 36 | "docs": "typedoc --options ./typedoc.json ./src/main/ts", 37 | "uglify": "for f in $(find target -name '*.js'); do short=${f%.js}; terser -c -m -o $short.js -- $f; done", 38 | "postupdate": "yarn && yarn build && yarn test" 39 | }, 40 | "dependencies": { 41 | "@qiwi/substrate": "^1.20.15", 42 | "@types/node": "^18.11.7", 43 | "@types/semantic-release": "^17.2.4", 44 | "lodash-es": "^4.17.21", 45 | "minimist": "^1.2.7", 46 | "tslib": "^2.4.0" 47 | }, 48 | "devDependencies": { 49 | "@qiwi/semrel-infra": "workspace:*" 50 | }, 51 | "repository": { 52 | "type": "git", 53 | "url": "git+https://github.com/qiwi/semantic-release-toolkit.git" 54 | }, 55 | "author": "Anton Golub ", 56 | "license": "MIT", 57 | "bugs": { 58 | "url": "https://github.com/qiwi/semantic-release-toolkit/issues" 59 | }, 60 | "homepage": "https://github.com/qiwi/semantic-release-toolkit/#readme", 61 | "prettier": "prettier-config-qiwi" 62 | } 63 | -------------------------------------------------------------------------------- /packages/common/src/main/ts/flags.ts: -------------------------------------------------------------------------------- 1 | import minimist from 'minimist' 2 | 3 | export const parseFlags = (argv: string[]): ReturnType => 4 | minimist(argv, { '--': true }) 5 | 6 | const checkValue = ( 7 | key: string, 8 | value: any, 9 | omitlist: any[], 10 | picklist: any[], 11 | ): boolean => 12 | value !== null && 13 | value !== undefined && 14 | value !== false && 15 | value !== 'false' && 16 | !omitlist.includes(key) && 17 | (picklist.length === 0 || picklist.includes(key)) 18 | 19 | const formatFlag = (key: string): string => 20 | (key.length === 1 ? '-' : '--') + key 21 | 22 | export const formatFlags = ( 23 | flags: Record, 24 | ...picklist: string[] 25 | ): string[] => 26 | Object.keys(flags).reduce((memo, key: string) => { 27 | const omitlist = ['_', '--'] 28 | const value = flags[key] 29 | const flag = formatFlag(key) 30 | 31 | if (checkValue(key, value, omitlist, picklist)) { 32 | memo.push(flag) 33 | 34 | if (value !== true) { 35 | memo.push(value) 36 | } 37 | } 38 | 39 | return memo 40 | }, []) 41 | -------------------------------------------------------------------------------- /packages/common/src/main/ts/ifaces.ts: -------------------------------------------------------------------------------- 1 | import { Extends } from '@qiwi/substrate' 2 | 3 | export interface ISyncSensitive { 4 | sync?: boolean 5 | } 6 | 7 | export type TSyncDirective = boolean | undefined | ISyncSensitive 8 | 9 | export type ParseSync = Extends< 10 | T, 11 | boolean | undefined, 12 | T, 13 | T extends ISyncSensitive ? T['sync'] : never 14 | > 15 | 16 | export type SyncGuard = Extends< 17 | S, 18 | { sync: true } | true, 19 | Extends, never, V>, 20 | Extends, V, Promise> 21 | > 22 | -------------------------------------------------------------------------------- /packages/common/src/main/ts/index.ts: -------------------------------------------------------------------------------- 1 | export * from './tpl' 2 | export * from './flags' 3 | -------------------------------------------------------------------------------- /packages/common/src/main/ts/tpl.ts: -------------------------------------------------------------------------------- 1 | import { IAnyMap } from '@qiwi/substrate' 2 | import { template as compile } from 'lodash-es' 3 | import { Context } from 'semantic-release' 4 | 5 | export const tpl = (template: string, context: IAnyMap, logger: Context['logger']): string => { 6 | try { 7 | return compile(template)(context) 8 | } catch (err) { 9 | logger.error('lodash.template render failure', err) 10 | 11 | return template 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/common/src/test/ts/unit/flags.ts: -------------------------------------------------------------------------------- 1 | import { formatFlags, parseFlags } from '../../../main/ts/flags' 2 | 3 | describe('formatFlags()', () => { 4 | it('return proper values', () => { 5 | const cases: [Record, string[], string[]][] = [ 6 | [{ _: [], '--': [] }, [], []], 7 | [{ foo: 'bar' }, [], ['--foo', 'bar']], 8 | [{ a: true, b: 'true', c: false, d: 'false' }, [], ['-a', '-b', 'true']], 9 | [{ verbose: true }, [], ['--verbose']], 10 | [ 11 | { f: true, foo: 'bar', b: true, baz: 'qux' }, 12 | ['f', 'baz'], 13 | ['-f', '--baz', 'qux'], 14 | ], 15 | [ 16 | parseFlags([ 17 | '-w', 18 | '1', 19 | '--force', 20 | '--audit-level=moderate', 21 | '--only=dev', 22 | '-c', 23 | 'ccc', 24 | '--', 25 | '--bar', 26 | '-b', 27 | '2', 28 | ]), 29 | ['force', 'audit-level', 'only', 'bar', 'b', 'c'], 30 | ['--force', '--audit-level', 'moderate', '--only', 'dev', '-c', 'ccc'], 31 | ], 32 | ] 33 | 34 | cases.forEach(([input, picklist, output]) => { 35 | expect(formatFlags(input, ...picklist)).toEqual(output) 36 | }) 37 | }) 38 | }) 39 | -------------------------------------------------------------------------------- /packages/common/src/test/ts/unit/index.ts: -------------------------------------------------------------------------------- 1 | import {formatFlags, parseFlags, tpl} from '../../../main/ts' 2 | 3 | describe('index', () => { 4 | it('properly exports its inners', () => { 5 | const utils = [formatFlags, parseFlags, tpl] 6 | 7 | utils.forEach((method) => expect(method).toEqual(expect.any(Function))) 8 | }) 9 | }) 10 | -------------------------------------------------------------------------------- /packages/common/src/test/ts/unit/tpl.ts: -------------------------------------------------------------------------------- 1 | import { jest } from '@jest/globals' 2 | 3 | import { tpl } from '../../../main/ts' 4 | 5 | describe('tpl', () => { 6 | const error = jest.fn((...vars: any[]) => { console.log(vars) }) 7 | const logger: any = { 8 | log (msg: string, ...vars: any[]) { console.log(vars || msg) }, 9 | error 10 | } 11 | 12 | it('inject data to string', () => { 13 | expect(tpl('foo <%= bar %>', { bar: 'baz' }, logger)).toBe('foo baz') 14 | }) 15 | 16 | it('returns template as is on failure', () => { 17 | const res = tpl('foo <%= bar.baz %>', { a: { b: 'c' } }, logger) 18 | 19 | expect(error).toHaveBeenCalledWith('lodash.template render failure', expect.any(Object)) 20 | expect(res).toBe('foo <%= bar.baz %>') 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /packages/common/tsconfig.es6.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "target": "es6", 5 | "module": "es6", 6 | "outDir": "target/es6" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/common/tsconfig.esnext.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "target": "esnext", 5 | "module": "esnext", 6 | "outDir": "target/esnext" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/common/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../packages/infra/tsconfig.compiler.json", 3 | "compilerOptions": { 4 | "rootDir": "./src/main/ts/", 5 | "baseUrl": "./src/main/ts/", 6 | "tsBuildInfoFile": "./buildcache/.tsbuildinfo", 7 | }, 8 | "references": [], 9 | "include": [ 10 | "src/main/**/*" 11 | ], 12 | "exclude": [ 13 | "node_modules" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /packages/common/tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "target": "esnext", 5 | "module": "esnext", 6 | "importHelpers": true, 7 | "noEmitHelpers": true, 8 | "esModuleInterop": true 9 | }, 10 | "include": [ 11 | "src/**/*" 12 | ], 13 | "exclude": [ 14 | "node_modules" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /packages/common/typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@qiwi/semrel-common", 3 | "out": "./docs", 4 | "exclude": ["src/test", "**/node_modules/**", "paralleljs"], 5 | "externalPattern": ["**/node_modules/**"], 6 | "excludePrivate": false, 7 | "hideGenerator": true, 8 | "readme": "README.md", 9 | "theme": "default", 10 | "tsconfig": "./tsconfig.es6.json" 11 | } 12 | -------------------------------------------------------------------------------- /packages/config-monorepo/README.md: -------------------------------------------------------------------------------- 1 | # @qiwi/semrel-config-monorepo 2 | Shared QIWI OSS config for [multi-semantic-release](https://github.com/qiwi/multi-semantic-release) 3 | 4 | ## Install 5 | ```shell script 6 | yarn add @qiwi/semrel-config-monorepo -D 7 | ``` 8 | 9 | ## Usage 10 | Inject this shared config in any way supported by [**msr** configuration flow](https://github.com/dhoulb/multi-semantic-release#configuration). 11 | ```json 12 | { 13 | "extends": "@qiwi/semrel-config--monorepo" 14 | } 15 | ``` 16 | -------------------------------------------------------------------------------- /packages/config-monorepo/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = {...require('../infra/jest.config.json')} 2 | -------------------------------------------------------------------------------- /packages/config-monorepo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@qiwi/semrel-config-monorepo", 3 | "version": "1.6.4", 4 | "publishConfig": { 5 | "access": "public" 6 | }, 7 | "description": "QIWI shared semrel config for monorepos", 8 | "keywords": [], 9 | "main": "target/es5/index.js", 10 | "source": "target/es5/index.js", 11 | "files": [ 12 | "README.md", 13 | "CHANGELOG.md", 14 | "target" 15 | ], 16 | "scripts": { 17 | "test": " yarn test:unit", 18 | "test:unit": "jest", 19 | "clean": "rimraf target", 20 | "build": "yarn build:es5", 21 | "build:es5": "mkdirp target/es5 && cpy src/main/js/ target/es5/ --flat", 22 | "postupdate": "yarn && yarn build && yarn test", 23 | "format": "prettier --write 'src/**/*.ts'" 24 | }, 25 | "dependencies": { 26 | "@qiwi/semrel-preset": "workspace:*" 27 | }, 28 | "devDependencies": { 29 | "@qiwi/semrel-infra": "workspace:*" 30 | }, 31 | "peerDependencies": { 32 | "semantic-release": "*" 33 | }, 34 | "repository": { 35 | "type": "git", 36 | "url": "git+https://github.com/qiwi/semantic-release-toolkit.git" 37 | }, 38 | "author": "Anton Golub ", 39 | "license": "MIT", 40 | "bugs": { 41 | "url": "https://github.com/qiwi/semantic-release-toolkit/issues" 42 | }, 43 | "homepage": "https://github.com/qiwi/semantic-release-toolkit/#readme" 44 | } 45 | -------------------------------------------------------------------------------- /packages/config-monorepo/src/main/js/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | branch: 'master', 3 | plugins: [ 4 | [ 5 | '@semantic-release/commit-analyzer', 6 | { 7 | preset: 'angular', 8 | releaseRules: [ 9 | {type: 'docs', release: 'patch'}, 10 | {type: 'refactor', release: 'patch'}, 11 | ], 12 | parserOpts: { 13 | noteKeywords: ['BREAKING CHANGE', 'BREAKING CHANGES'] 14 | } 15 | } 16 | ], 17 | '@semantic-release/release-notes-generator', 18 | '@semantic-release/changelog', 19 | [ 20 | '@semantic-release/exec', 21 | { 22 | prepareCmd: 'CI=true YARN_ENABLE_IMMUTABLE_INSTALLS=false yarn install && git add ../../yarn.lock', 23 | } 24 | ], 25 | '@semrel-extra/npm', 26 | [ 27 | '@semantic-release/github', 28 | { 29 | successComment: false, 30 | failComment: false 31 | } 32 | ], 33 | [ 34 | '@semantic-release/git', 35 | { 36 | message: 'chore(release): ${nextRelease.gitTag} [skip ci]\n\n${nextRelease.notes}' 37 | } 38 | ] 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /packages/config-monorepo/src/test/js/index.js: -------------------------------------------------------------------------------- 1 | const config = require('../../main/js') 2 | 3 | describe('@qiwi/semrel-config-monorepo', () => { 4 | it('is not empty', () => { 5 | expect(config).not.toBeUndefined() 6 | expect(config).toEqual(require('../../../target/es5')) 7 | }) 8 | }) 9 | -------------------------------------------------------------------------------- /packages/config/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## @qiwi/semrel-config [1.4.1](https://github.com/qiwi/semantic-release-toolkit/compare/@qiwi/semrel-config@1.4.0...@qiwi/semrel-config@1.4.1) (2022-05-09) 2 | 3 | 4 | ### Performance Improvements 5 | 6 | * deps revision ([7974aab](https://github.com/qiwi/semantic-release-toolkit/commit/7974aab6ad056e840061ba03218e14cf81a2fd07)) 7 | 8 | 9 | 10 | 11 | 12 | ### Dependencies 13 | 14 | * **@qiwi/semrel-preset:** upgraded to 3.1.10 15 | 16 | # @qiwi/semrel-config [1.4.0](https://github.com/qiwi/semantic-release-toolkit/compare/@qiwi/semrel-config@1.3.23...@qiwi/semrel-config@1.4.0) (2022-04-12) 17 | 18 | 19 | ### Features 20 | 21 | * replace @qiwi/semantic-release-gh-pages-plugin with @qiwi/semrel-metabranch ([ca7bd1c](https://github.com/qiwi/semantic-release-toolkit/commit/ca7bd1c22b2483e5d0d6902c017bbef0b726d354)) 22 | 23 | ## @qiwi/semrel-config [1.3.23](https://github.com/qiwi/semantic-release-toolkit/compare/@qiwi/semrel-config@1.3.22...@qiwi/semrel-config@1.3.23) (2022-04-09) 24 | 25 | 26 | 27 | 28 | 29 | ### Dependencies 30 | 31 | * **@qiwi/semrel-preset:** upgraded to 3.1.9 32 | * **@qiwi/semrel-infra:** upgraded to 3.2.1 33 | 34 | ## @qiwi/semrel-config [1.3.22](https://github.com/qiwi/semantic-release-toolkit/compare/@qiwi/semrel-config@1.3.21...@qiwi/semrel-config@1.3.22) (2022-03-12) 35 | 36 | 37 | 38 | 39 | 40 | ### Dependencies 41 | 42 | * **@qiwi/semrel-preset:** upgraded to 3.1.8 43 | * **@qiwi/semrel-infra:** upgraded to 3.2.0 44 | 45 | ## @qiwi/semrel-config [1.3.21](https://github.com/qiwi/semantic-release-toolkit/compare/@qiwi/semrel-config@1.3.20...@qiwi/semrel-config@1.3.21) (2022-03-06) 46 | 47 | 48 | 49 | 50 | 51 | ### Dependencies 52 | 53 | * **@qiwi/semrel-preset:** upgraded to 3.1.7 54 | * **@qiwi/semrel-infra:** upgraded to 3.1.0 55 | 56 | ## @qiwi/semrel-config [1.3.20](https://github.com/qiwi/semantic-release-toolkit/compare/@qiwi/semrel-config@1.3.19...@qiwi/semrel-config@1.3.20) (2022-03-06) 57 | 58 | 59 | 60 | 61 | 62 | ### Dependencies 63 | 64 | * **@qiwi/semrel-preset:** upgraded to 3.1.6 65 | * **@qiwi/semrel-infra:** upgraded to 3.0.8 66 | 67 | ## @qiwi/semrel-config [1.3.19](https://github.com/qiwi/semantic-release-toolkit/compare/@qiwi/semrel-config@1.3.18...@qiwi/semrel-config@1.3.19) (2022-02-22) 68 | 69 | 70 | 71 | 72 | 73 | ### Dependencies 74 | 75 | * **@qiwi/semrel-preset:** upgraded to 3.1.5 76 | 77 | ## @qiwi/semrel-config [1.3.18](https://github.com/qiwi/semantic-release-toolkit/compare/@qiwi/semrel-config@1.3.17...@qiwi/semrel-config@1.3.18) (2021-12-29) 78 | 79 | 80 | 81 | 82 | 83 | ### Dependencies 84 | 85 | * **@qiwi/semrel-preset:** upgraded to 3.1.4 86 | 87 | ## @qiwi/semrel-config [1.3.17](https://github.com/qiwi/semantic-release-toolkit/compare/@qiwi/semrel-config@1.3.16...@qiwi/semrel-config@1.3.17) (2021-12-23) 88 | 89 | 90 | 91 | 92 | 93 | ### Dependencies 94 | 95 | * **@qiwi/semrel-preset:** upgraded to 3.1.3 96 | 97 | ## @qiwi/semrel-config [1.3.16](https://github.com/qiwi/semantic-release-toolkit/compare/@qiwi/semrel-config@1.3.15...@qiwi/semrel-config@1.3.16) (2021-12-22) 98 | 99 | 100 | 101 | 102 | 103 | ### Dependencies 104 | 105 | * **@qiwi/semrel-preset:** upgraded to 3.1.2 106 | 107 | ## @qiwi/semrel-config [1.3.15](https://github.com/qiwi/semantic-release-toolkit/compare/@qiwi/semrel-config@1.3.14...@qiwi/semrel-config@1.3.15) (2021-11-18) 108 | 109 | 110 | 111 | 112 | 113 | ### Dependencies 114 | 115 | * **@qiwi/semrel-preset:** upgraded to 3.1.1 116 | 117 | ## @qiwi/semrel-config [1.3.14](https://github.com/qiwi/semantic-release-toolkit/compare/@qiwi/semrel-config@1.3.13...@qiwi/semrel-config@1.3.14) (2021-11-18) 118 | 119 | 120 | 121 | 122 | 123 | ### Dependencies 124 | 125 | * **@qiwi/semrel-preset:** upgraded to 3.1.0 126 | 127 | ## @qiwi/semrel-config [1.3.13](https://github.com/qiwi/semantic-release-toolkit/compare/@qiwi/semrel-config@1.3.12...@qiwi/semrel-config@1.3.13) (2021-11-17) 128 | 129 | 130 | 131 | 132 | 133 | ### Dependencies 134 | 135 | * **@qiwi/semrel-preset:** upgraded to 3.0.0 136 | 137 | ## @qiwi/semrel-config [1.3.12](https://github.com/qiwi/semantic-release-toolkit/compare/@qiwi/semrel-config@1.3.11...@qiwi/semrel-config@1.3.12) (2021-10-28) 138 | 139 | 140 | 141 | 142 | 143 | ### Dependencies 144 | 145 | * **@qiwi/semrel-preset:** upgraded to 2.1.0 146 | 147 | ## @qiwi/semrel-config [1.3.11](https://github.com/qiwi/semantic-release-toolkit/compare/@qiwi/semrel-config@1.3.10...@qiwi/semrel-config@1.3.11) (2021-09-27) 148 | 149 | 150 | 151 | 152 | 153 | ### Dependencies 154 | 155 | * **@qiwi/semrel-preset:** upgraded to 2.0.0 156 | 157 | ## @qiwi/semrel-config [1.3.10](https://github.com/qiwi/semantic-release-toolkit/compare/@qiwi/semrel-config@1.3.9...@qiwi/semrel-config@1.3.10) (2021-08-25) 158 | 159 | 160 | 161 | 162 | 163 | ### Dependencies 164 | 165 | * **@qiwi/semrel-preset:** upgraded to 1.3.2 166 | 167 | ## @qiwi/semrel-config [1.3.9](https://github.com/qiwi/semantic-release-toolkit/compare/@qiwi/semrel-config@1.3.8...@qiwi/semrel-config@1.3.9) (2021-07-07) 168 | 169 | 170 | 171 | 172 | 173 | ### Dependencies 174 | 175 | * **@qiwi/semrel-preset:** upgraded to 1.3.1 176 | 177 | ## @qiwi/semrel-config [1.3.8](https://github.com/qiwi/semantic-release-toolkit/compare/@qiwi/semrel-config@1.3.7...@qiwi/semrel-config@1.3.8) (2021-06-18) 178 | 179 | 180 | 181 | 182 | 183 | ### Dependencies 184 | 185 | * **@qiwi/semrel-preset:** upgraded to 1.3.0 186 | 187 | ## @qiwi/semrel-config [1.3.7](https://github.com/qiwi/semantic-release-toolkit/compare/@qiwi/semrel-config@1.3.6...@qiwi/semrel-config@1.3.7) (2021-06-17) 188 | 189 | 190 | 191 | 192 | 193 | ### Dependencies 194 | 195 | * **@qiwi/semrel-preset:** upgraded to 1.2.6 196 | 197 | ## @qiwi/semrel-config [1.3.6](https://github.com/qiwi/semantic-release-toolkit/compare/@qiwi/semrel-config@1.3.5...@qiwi/semrel-config@1.3.6) (2021-05-17) 198 | 199 | 200 | 201 | 202 | 203 | ### Dependencies 204 | 205 | * **@qiwi/semrel-preset:** upgraded to 1.2.5 206 | 207 | ## @qiwi/semrel-config [1.3.5](https://github.com/qiwi/semantic-release-toolkit/compare/@qiwi/semrel-config@1.3.4...@qiwi/semrel-config@1.3.5) (2021-04-04) 208 | 209 | 210 | 211 | 212 | 213 | ### Dependencies 214 | 215 | * **@qiwi/semrel-preset:** upgraded to 1.2.4 216 | 217 | ## @qiwi/semrel-config [1.3.4](https://github.com/qiwi/semantic-release-toolkit/compare/@qiwi/semrel-config@1.3.3...@qiwi/semrel-config@1.3.4) (2021-04-02) 218 | 219 | 220 | 221 | 222 | 223 | ### Dependencies 224 | 225 | * **@qiwi/semrel-preset:** upgraded to 1.2.3 226 | 227 | ## @qiwi/semrel-config [1.3.3](https://github.com/qiwi/semantic-release-toolkit/compare/@qiwi/semrel-config@1.3.2...@qiwi/semrel-config@1.3.3) (2021-04-02) 228 | 229 | 230 | 231 | 232 | 233 | ### Dependencies 234 | 235 | * **@qiwi/semrel-preset:** upgraded to 1.2.2 236 | 237 | ## @qiwi/semrel-config [1.3.2](https://github.com/qiwi/semantic-release-toolkit/compare/@qiwi/semrel-config@1.3.1...@qiwi/semrel-config@1.3.2) (2021-04-02) 238 | 239 | 240 | 241 | 242 | 243 | ### Dependencies 244 | 245 | * **@qiwi/semrel-preset:** upgraded to 1.2.1 246 | 247 | ## @qiwi/semrel-config [1.3.1](https://github.com/qiwi/semantic-release-toolkit/compare/@qiwi/semrel-config@1.3.0...@qiwi/semrel-config@1.3.1) (2021-04-02) 248 | 249 | 250 | 251 | 252 | 253 | ### Dependencies 254 | 255 | * **@qiwi/semrel-preset:** upgraded to 1.2.0 256 | 257 | # @qiwi/semrel-config [1.3.0](https://github.com/qiwi/semantic-release-toolkit/compare/@qiwi/semrel-config@1.2.2...@qiwi/semrel-config@1.3.0) (2021-03-22) 258 | 259 | 260 | ### Features 261 | 262 | * **config:** release refactoring as patche ([c2bbc97](https://github.com/qiwi/semantic-release-toolkit/commit/c2bbc97e4e265e839e034671bf629210ae99db45)) 263 | 264 | 265 | 266 | 267 | 268 | ### Dependencies 269 | 270 | * **@qiwi/semrel-preset:** upgraded to 1.1.2 271 | 272 | ## @qiwi/semrel-config [1.2.2](https://github.com/qiwi/semantic-release-toolkit/compare/@qiwi/semrel-config@1.2.1...@qiwi/semrel-config@1.2.2) (2021-03-12) 273 | 274 | 275 | 276 | 277 | 278 | ### Dependencies 279 | 280 | * **@qiwi/semrel-preset:** upgraded to 1.1.1 281 | 282 | ## @qiwi/semrel-config [1.2.1](https://github.com/qiwi/semantic-release-toolkit/compare/@qiwi/semrel-config@1.2.0...@qiwi/semrel-config@1.2.1) (2021-03-11) 283 | 284 | 285 | 286 | 287 | 288 | ### Dependencies 289 | 290 | * **@qiwi/semrel-preset:** upgraded to 1.1.0 291 | 292 | # @qiwi/semrel-config [1.2.0](https://github.com/qiwi/semantic-release-toolkit/compare/@qiwi/semrel-config@1.1.0...@qiwi/semrel-config@1.2.0) (2021-01-31) 293 | 294 | 295 | ### Features 296 | 297 | * **config:** include semrel-preset to pkg deps ([6998b42](https://github.com/qiwi/semantic-release-toolkit/commit/6998b4212df4665274b43978ca7ab2fad58b37ec)) 298 | 299 | # @qiwi/semrel-config [1.1.0](https://github.com/qiwi/semantic-release-toolkit/compare/@qiwi/semrel-config@1.0.0...@qiwi/semrel-config@1.1.0) (2020-10-29) 300 | 301 | 302 | ### Features 303 | 304 | * introduce plugin preset package ([dff254f](https://github.com/qiwi/semantic-release-toolkit/commit/dff254ff4b4d5088e165acb97e28f9e40f84bd20)) 305 | 306 | # @qiwi/semrel-config 1.0.0 (2020-10-28) 307 | 308 | 309 | ### Features 310 | 311 | * add semrel-config and semrel-config-monorepo ([ad7ba33](https://github.com/qiwi/semantic-release-toolkit/commit/ad7ba33cf6f6705c1f1f1919c197d5ad7345de4b)) 312 | -------------------------------------------------------------------------------- /packages/config/README.md: -------------------------------------------------------------------------------- 1 | # @qiwi/semrel-config 2 | Shared QIWI OSS config for [semantic-release](https://github.com/semantic-release/semantic-release) 3 | 4 | ## Install 5 | ```shell script 6 | yarn add @qiwi/semrel-config -D 7 | ``` 8 | 9 | ## Usage 10 | Inject this shared config in any way supported by [semrel configuration flow](https://github.com/semantic-release/semantic-release/blob/master/docs/usage/configuration.md#configuration). For example, **.releaserc**: 11 | ```json 12 | { 13 | "extends": "@qiwi/semrel-config" 14 | } 15 | ``` 16 | -------------------------------------------------------------------------------- /packages/config/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = {...require('../infra/jest.config.json')} 2 | -------------------------------------------------------------------------------- /packages/config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@qiwi/semrel-config", 3 | "version": "1.4.1", 4 | "publishConfig": { 5 | "access": "public" 6 | }, 7 | "description": "QIWI shared semrel config", 8 | "keywords": [], 9 | "main": "target/es5/index.js", 10 | "source": "target/es5/index.js", 11 | "files": [ 12 | "README.md", 13 | "CHANGELOG.md", 14 | "target" 15 | ], 16 | "scripts": { 17 | "test": "yarn test:unit", 18 | "test:unit": "jest --runInBand", 19 | "clean": "rimraf target coverage", 20 | "build": "yarn build:es5", 21 | "build:es5": "mkdirp target/es5 && cpy src/main/js/ target/es5/ --flat", 22 | "postupdate": "yarn && yarn build && yarn test", 23 | "format": "prettier --write 'src/**/*.ts'" 24 | }, 25 | "dependencies": { 26 | "@qiwi/semrel-preset": "workspace:*" 27 | }, 28 | "devDependencies": { 29 | "@qiwi/semrel-infra": "workspace:*" 30 | }, 31 | "peerDependencies": { 32 | "semantic-release": "*" 33 | }, 34 | "repository": { 35 | "type": "git", 36 | "url": "git+https://github.com/qiwi/semantic-release-toolkit.git" 37 | }, 38 | "author": "Anton Golub ", 39 | "license": "MIT", 40 | "bugs": { 41 | "url": "https://github.com/qiwi/semantic-release-toolkit/issues" 42 | }, 43 | "homepage": "https://github.com/qiwi/semantic-release-toolkit/#readme" 44 | } 45 | -------------------------------------------------------------------------------- /packages/config/src/main/js/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | branch: 'master', 3 | plugins: [ 4 | [ 5 | '@semantic-release/commit-analyzer', 6 | { 7 | preset: 'angular', 8 | releaseRules: [ 9 | {type: 'docs', release: 'patch'}, 10 | {type: 'refactor', release: 'patch'}, 11 | ], 12 | parserOpts: { 13 | noteKeywords: ['BREAKING CHANGE', 'BREAKING CHANGES'] 14 | } 15 | } 16 | ], 17 | '@semantic-release/release-notes-generator', 18 | '@semantic-release/changelog', 19 | [ 20 | '@qiwi/semrel-metabranch', 21 | { 22 | publish: { 23 | action: 'push', 24 | branch: 'gh-pages', 25 | from: './docs', 26 | to: './', 27 | message: 'update docs ${nextRelease.gitTag}', 28 | } 29 | } 30 | ], 31 | '@semantic-release/npm', 32 | '@semantic-release/github', 33 | '@semantic-release/git' 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /packages/config/src/test/js/index.js: -------------------------------------------------------------------------------- 1 | const config = require('../../main/js') 2 | 3 | describe('@qiwi/semrel-config', () => { 4 | it('is not empty', () => { 5 | expect(config).not.toBeUndefined() 6 | expect(config).toEqual(require('../../../target/es5')) 7 | }) 8 | }) 9 | -------------------------------------------------------------------------------- /packages/git-sync/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## @qiwi/git-sync [1.2.3](https://github.com/qiwi/semantic-release-toolkit/compare/@qiwi/git-sync@1.2.2...@qiwi/git-sync@1.2.3) (2022-05-09) 2 | 3 | 4 | ### Performance Improvements 5 | 6 | * update msr to v6.2.0 ([04700e6](https://github.com/qiwi/semantic-release-toolkit/commit/04700e68b547ae82910dcf51d39813a3b84c6b0d)) 7 | 8 | 9 | 10 | 11 | 12 | ### Dependencies 13 | 14 | * **@qiwi/semrel-metabranch:** upgraded to 3.1.2 15 | * **@qiwi/semrel-testing-suite:** upgraded to 3.1.2 16 | 17 | ## @qiwi/git-sync [1.2.2](https://github.com/qiwi/semantic-release-toolkit/compare/@qiwi/git-sync@1.2.1...@qiwi/git-sync@1.2.2) (2022-04-09) 18 | 19 | 20 | ### Bug Fixes 21 | 22 | * update msr ([ee7c860](https://github.com/qiwi/semantic-release-toolkit/commit/ee7c860edbcb8f423e82bd13a4e5df27d2cb6edc)) 23 | 24 | 25 | 26 | 27 | 28 | ### Dependencies 29 | 30 | * **@qiwi/semrel-metabranch:** upgraded to 3.1.1 31 | * **@qiwi/semrel-infra:** upgraded to 3.2.1 32 | * **@qiwi/semrel-testing-suite:** upgraded to 3.1.1 33 | 34 | ## @qiwi/git-sync [1.2.1](https://github.com/qiwi/semantic-release-toolkit/compare/@qiwi/git-sync@1.2.0...@qiwi/git-sync@1.2.1) (2022-04-04) 35 | 36 | 37 | ### Bug Fixes 38 | 39 | * **deps:** update dependency hosted-git-info to v5 ([#122](https://github.com/qiwi/semantic-release-toolkit/issues/122)) ([82afc3d](https://github.com/qiwi/semantic-release-toolkit/commit/82afc3d46ca5cccfe4c98a21af9182593c6afad8)) 40 | 41 | # @qiwi/git-sync [1.2.0](https://github.com/qiwi/semantic-release-toolkit/compare/@qiwi/git-sync@1.1.0...@qiwi/git-sync@1.2.0) (2022-03-12) 42 | 43 | 44 | ### Features 45 | 46 | * exclude git-utils from common assets ([7cd7eab](https://github.com/qiwi/semantic-release-toolkit/commit/7cd7eabe167dae403eafb6c2d27b5829f2a3181b)) 47 | 48 | 49 | 50 | 51 | 52 | ### Dependencies 53 | 54 | * **@qiwi/semrel-metabranch:** upgraded to 3.1.0 55 | * **@qiwi/semrel-infra:** upgraded to 3.2.0 56 | * **@qiwi/semrel-testing-suite:** upgraded to 3.1.0 57 | 58 | # @qiwi/git-sync [1.1.0](https://github.com/qiwi/semantic-release-toolkit/compare/@qiwi/git-sync@1.0.0...@qiwi/git-sync@1.1.0) (2022-03-06) 59 | 60 | 61 | ### Features 62 | 63 | * introduce @qiwi/git-utils package ([e5d68f8](https://github.com/qiwi/semantic-release-toolkit/commit/e5d68f864fecd8f7be5ce97a533bda1ce6568096)), closes [#120](https://github.com/qiwi/semantic-release-toolkit/issues/120) 64 | 65 | 66 | 67 | 68 | 69 | ### Dependencies 70 | 71 | * **@qiwi/semrel-metabranch:** upgraded to 3.0.6 72 | * **@qiwi/semrel-infra:** upgraded to 3.1.0 73 | * **@qiwi/semrel-testing-suite:** upgraded to 3.0.6 74 | -------------------------------------------------------------------------------- /packages/git-sync/README.md: -------------------------------------------------------------------------------- 1 | # @qiwi/git-sync 2 | Git helper to fetch & push dirs. For everything else there is [nodegit](https://github.com/nodegit/nodegit). 3 | 4 | ## Usage 5 | ### JS API 6 | ```javascript 7 | import { gitSync } from '@qiwi/git-sync' 8 | 9 | // Upload 10 | await gitSync({ 11 | action: 'push', 12 | from: './docs', 13 | to: './', 14 | brach: 'gh-pages', 15 | url: 'https://https://github.com/some/repo.git', 16 | env: { 17 | GIT_TOKEN: 'token' 18 | } 19 | }) 20 | 21 | // Download 22 | await gitSync({ 23 | action: 'fetch', 24 | url: 'https://https://github.com/some/repo.git', 25 | brach: 'gh-pages', 26 | from: './', 27 | to: './docs', 28 | env: { 29 | GIT_TOKEN: 'token' 30 | } 31 | }) 32 | ``` 33 | 34 | ### CLI 35 | **Fetch** from remote to local: 36 | ```shell 37 | GIT_TOKEN='token' gitsync --url='https://https://github.com/some/repo.git' --action='fetch' --from='./' --to='./docs' --branch='gh-pages' 38 | ``` 39 | the same via ssh: 40 | ```shell 41 | GIT_SSH_COMMAND='ssh -i private_key_file -o IdentitiesOnly=yes' gitsync --url='git@github.com:some/repo.git' --action='fetch' --from='./' --to='./docs' 42 | ``` 43 | 44 | **Push** local dir to remote: 45 | ```shell 46 | GIT_TOKEN='token' gitsync --action='push' --from='./docs' --to='./' --branch='gh-pages' 47 | ``` 48 | 49 | ## License 50 | [MIT](./../../LICENSE) 51 | -------------------------------------------------------------------------------- /packages/git-sync/jest.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = {...require('../infra/jest.config.json')} 2 | -------------------------------------------------------------------------------- /packages/git-sync/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@qiwi/git-sync", 3 | "version": "1.2.3", 4 | "publishConfig": { 5 | "access": "public" 6 | }, 7 | "description": "Git fetch/push helper", 8 | "keywords": [], 9 | "type": "module", 10 | "exports": "./target/es6/index.js", 11 | "module": "./target/es6/index.js", 12 | "source": "./target/es6/index.js", 13 | "files": [ 14 | "README.md", 15 | "CHANGELOG.md", 16 | "target" 17 | ], 18 | "scripts": { 19 | "clean": "rimraf target typings flow-typed buildcache coverage docs", 20 | "_lint": "eslint 'src/**/*.js'", 21 | "lint": "echo 'linter disabled'", 22 | "lint:fix": "yarn lint --fix", 23 | "format": "prettier --write 'src/**/*.js'", 24 | "test": "concurrently yarn:lint yarn:test:unit yarn:test:depcheck", 25 | "test:unit": "NODE_OPTIONS=--experimental-vm-modules jest --detectOpenHandles --forceExit --runInBand", 26 | "_test:depcheck": "npm_config_yes=true npx -p depcheck -p @babel/parser@7.16.4 depcheck --ignores '@jest/globals,@types/*,tslib,eslint-*,prettier-*,@qiwi/semrel-infra,@qiwi/semrel-common' --ignore-patterns 'typings,flow-typed/*'", 27 | "test:depcheck": "echo 'depcheck disabled' && exit 0", 28 | "build": "concurrently yarn:build:es6", 29 | "build:es6": "cpy ./src/main/js/ ./target/es6/ --flat", 30 | "postupdate": "yarn && yarn build && yarn test" 31 | }, 32 | "dependencies": { 33 | "@qiwi/semrel-metabranch": "workspace:*", 34 | "debug": "^4.3.4", 35 | "execa": "^6.1.0", 36 | "hosted-git-info": "^5.2.0" 37 | }, 38 | "devDependencies": { 39 | "@qiwi/semrel-infra": "workspace:*", 40 | "@qiwi/semrel-testing-suite": "workspace:*", 41 | "@types/node": "^18.11.7", 42 | "resolve-from": "^5.0.0" 43 | }, 44 | "repository": { 45 | "type": "git", 46 | "url": "git+https://github.com/qiwi/semantic-release-toolkit.git" 47 | }, 48 | "author": "Anton Golub ", 49 | "license": "MIT", 50 | "bugs": { 51 | "url": "https://github.com/qiwi/semantic-release-toolkit/issues" 52 | }, 53 | "homepage": "https://github.com/qiwi/semantic-release-toolkit/#readme", 54 | "prettier": "prettier-config-qiwi" 55 | } 56 | -------------------------------------------------------------------------------- /packages/git-sync/src/main/js/get-git-auth-url.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copy-pasted as is from semantic-release sources 3 | * https://github.com/semantic-release/semantic-release/blob/master/lib/get-git-auth-url.js 4 | */ 5 | 6 | const {parse, format} = require('url'); // eslint-disable-line node/no-deprecated-api 7 | const hostedGitInfo = require('hosted-git-info'); 8 | const debug = require('debug')('get-git-auth-url'); 9 | 10 | function isNil(value) { 11 | return value == null; 12 | } 13 | 14 | /** 15 | * Verify the write access authorization to remote repository with push dry-run. 16 | * 17 | * @param {String} repositoryUrl The remote repository URL. 18 | * @param {String} branch The repository branch for which to verify write access. 19 | * @param {Object} [execaOpts] Options to pass to `execa`. 20 | * 21 | * @throws {Error} if not authorized to push. 22 | */ 23 | async function verifyAuth(repositoryUrl, branch, execaOptions) { 24 | try { 25 | await execa('git', ['push', '--dry-run', '--no-verify', repositoryUrl, `HEAD:${branch}`], execaOptions); 26 | } catch (error) { 27 | debug(error); 28 | throw error; 29 | } 30 | } 31 | 32 | /** 33 | * Machinery to format a repository URL with the given credentials 34 | * 35 | * @param {String} protocol URL protocol (which should not be present in repositoryUrl) 36 | * @param {String} repositoryUrl User-given repository URL 37 | * @param {String} gitCredentials The basic auth part of the URL 38 | * 39 | * @return {String} The formatted Git repository URL. 40 | */ 41 | function formatAuthUrl(protocol, repositoryUrl, gitCredentials) { 42 | const [match, auth, host, basePort, path] = 43 | /^(?!.+:\/\/)(?:(?.*)@)?(?.*?):(?\d+)?:?\/?(?.*)$/.exec(repositoryUrl) || []; 44 | const {port, hostname, ...parsed} = parse( 45 | match ? `ssh://${auth ? `${auth}@` : ''}${host}${basePort ? `:${basePort}` : ''}/${path}` : repositoryUrl 46 | ); 47 | 48 | return format({ 49 | ...parsed, 50 | auth: gitCredentials, 51 | host: `${hostname}${protocol === 'ssh:' ? '' : port ? `:${port}` : ''}`, 52 | protocol: protocol && /http[^s]/.test(protocol) ? 'http' : 'https', 53 | }); 54 | } 55 | 56 | /** 57 | * Verify authUrl by calling git.verifyAuth, but don't throw on failure 58 | * 59 | * @param {Object} context semantic-release context. 60 | * @param {String} authUrl Repository URL to verify 61 | * 62 | * @return {String} The authUrl as is if the connection was successfull, null otherwise 63 | */ 64 | async function ensureValidAuthUrl({cwd, env, branch}, authUrl) { 65 | try { 66 | await verifyAuth(authUrl, branch.name, {cwd, env}); 67 | return authUrl; 68 | } catch (error) { 69 | debug(error); 70 | return null; 71 | } 72 | } 73 | 74 | /** 75 | * Determine the the git repository URL to use to push, either: 76 | * - The `repositoryUrl` as is if allowed to push 77 | * - The `repositoryUrl` converted to `https` or `http` with Basic Authentication 78 | * 79 | * In addition, expand shortcut URLs (`owner/repo` => `https://github.com/owner/repo.git`) and transform `git+https` / `git+http` URLs to `https` / `http`. 80 | * 81 | * @param {Object} context semantic-release context. 82 | * 83 | * @return {String} The formatted Git repository URL. 84 | */ 85 | module.exports = async (context) => { 86 | const {cwd, env, branch} = context; 87 | const GIT_TOKENS = { 88 | GIT_CREDENTIALS: undefined, 89 | GH_TOKEN: undefined, 90 | // GitHub Actions require the "x-access-token:" prefix for git access 91 | // https://developer.github.com/apps/building-github-apps/authenticating-with-github-apps/#http-based-git-access-by-an-installation 92 | GITHUB_TOKEN: isNil(env.GITHUB_ACTION) ? undefined : 'x-access-token:', 93 | GL_TOKEN: 'gitlab-ci-token:', 94 | GITLAB_TOKEN: 'gitlab-ci-token:', 95 | BB_TOKEN: 'x-token-auth:', 96 | BITBUCKET_TOKEN: 'x-token-auth:', 97 | BB_TOKEN_BASIC_AUTH: '', 98 | BITBUCKET_TOKEN_BASIC_AUTH: '', 99 | }; 100 | 101 | let {repositoryUrl} = context.options; 102 | const info = hostedGitInfo.fromUrl(repositoryUrl, {noGitPlus: true}); 103 | const {protocol, ...parsed} = parse(repositoryUrl); 104 | 105 | if (info && info.getDefaultRepresentation() === 'shortcut') { 106 | // Expand shorthand URLs (such as `owner/repo` or `gitlab:owner/repo`) 107 | repositoryUrl = info.https(); 108 | } else if (protocol && protocol.includes('http')) { 109 | // Replace `git+https` and `git+http` with `https` or `http` 110 | repositoryUrl = format({...parsed, protocol: protocol.includes('https') ? 'https' : 'http', href: null}); 111 | } 112 | 113 | // Test if push is allowed without transforming the URL (e.g. is ssh keys are set up) 114 | try { 115 | debug('Verifying ssh auth by attempting to push to %s', repositoryUrl); 116 | await verifyAuth(repositoryUrl, branch.name, {cwd, env}); 117 | } catch (_) { 118 | debug('SSH key auth failed, falling back to https.'); 119 | const envVars = Object.keys(GIT_TOKENS).filter((envVar) => !isNil(env[envVar])); 120 | 121 | // Skip verification if there is no ambiguity on which env var to use for authentication 122 | if (envVars.length === 1) { 123 | const gitCredentials = `${GIT_TOKENS[envVars[0]] || ''}${env[envVars[0]]}`; 124 | return formatAuthUrl(protocol, repositoryUrl, gitCredentials); 125 | } 126 | 127 | if (envVars.length > 1) { 128 | debug(`Found ${envVars.length} credentials in environment, trying all of them`); 129 | 130 | const candidateRepositoryUrls = []; 131 | for (const envVar of envVars) { 132 | const gitCredentials = `${GIT_TOKENS[envVar] || ''}${env[envVar]}`; 133 | const authUrl = formatAuthUrl(protocol, repositoryUrl, gitCredentials); 134 | candidateRepositoryUrls.push(ensureValidAuthUrl(context, authUrl)); 135 | } 136 | 137 | const validRepositoryUrls = await Promise.all(candidateRepositoryUrls); 138 | const chosenAuthUrlIndex = validRepositoryUrls.findIndex((url) => url !== null); 139 | if (chosenAuthUrlIndex > -1) { 140 | debug(`Using "${envVars[chosenAuthUrlIndex]}" to authenticate`); 141 | return validRepositoryUrls[chosenAuthUrlIndex]; 142 | } 143 | } 144 | } 145 | 146 | return repositoryUrl; 147 | }; 148 | -------------------------------------------------------------------------------- /packages/git-sync/src/main/js/index.js: -------------------------------------------------------------------------------- 1 | export const foo = 'bar' 2 | -------------------------------------------------------------------------------- /packages/git-sync/src/test/js/index.js: -------------------------------------------------------------------------------- 1 | import {foo} from '../../main/js/index.js' 2 | 3 | describe('foo', () => { 4 | it('bar', () => { 5 | expect(foo).toBe('bar') 6 | }) 7 | }) 8 | -------------------------------------------------------------------------------- /packages/git-utils/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | 'eslint-config-qiwi', 4 | 'prettier', 5 | ], 6 | overrides: [ 7 | { 8 | files: ['./src/test/**/*.ts'], 9 | rules: { 10 | 'unicorn/consistent-function-scoping': 'off', 11 | 'sonarjs/no-duplicate-string': 'off' 12 | } 13 | } 14 | ] 15 | }; 16 | -------------------------------------------------------------------------------- /packages/git-utils/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## @qiwi/git-utils [1.1.2](https://github.com/qiwi/semantic-release-toolkit/compare/@qiwi/git-utils@1.1.1...@qiwi/git-utils@1.1.2) (2022-05-09) 2 | 3 | 4 | ### Performance Improvements 5 | 6 | * deps revision ([7974aab](https://github.com/qiwi/semantic-release-toolkit/commit/7974aab6ad056e840061ba03218e14cf81a2fd07)) 7 | * update msr to v6.2.0 ([04700e6](https://github.com/qiwi/semantic-release-toolkit/commit/04700e68b547ae82910dcf51d39813a3b84c6b0d)) 8 | 9 | 10 | 11 | 12 | 13 | ### Dependencies 14 | 15 | * **@qiwi/semrel-common:** upgraded to 3.4.2 16 | 17 | ## @qiwi/git-utils [1.1.1](https://github.com/qiwi/semantic-release-toolkit/compare/@qiwi/git-utils@1.1.0...@qiwi/git-utils@1.1.1) (2022-04-09) 18 | 19 | 20 | 21 | 22 | 23 | ### Dependencies 24 | 25 | * **@qiwi/semrel-common:** upgraded to 3.4.1 26 | * **@qiwi/semrel-infra:** upgraded to 3.2.1 27 | 28 | # @qiwi/git-utils [1.1.0](https://github.com/qiwi/semantic-release-toolkit/compare/@qiwi/git-utils@1.0.0...@qiwi/git-utils@1.1.0) (2022-03-12) 29 | 30 | 31 | ### Features 32 | 33 | * exclude git-utils from common assets ([7cd7eab](https://github.com/qiwi/semantic-release-toolkit/commit/7cd7eabe167dae403eafb6c2d27b5829f2a3181b)) 34 | * **git-utils:** allow empty commits, add gitInitOrigin stage to gitInitTestingRepo flow ([7d0b972](https://github.com/qiwi/semantic-release-toolkit/commit/7d0b97273d3d23278160edc293c969b06fcf5284)) 35 | 36 | 37 | 38 | 39 | 40 | ### Dependencies 41 | 42 | * **@qiwi/semrel-common:** upgraded to 3.4.0 43 | * **@qiwi/semrel-infra:** upgraded to 3.2.0 44 | 45 | # @qiwi/git-utils 1.0.0 (2022-03-06) 46 | 47 | 48 | ### Features 49 | 50 | * introduce @qiwi/git-utils package ([e5d68f8](https://github.com/qiwi/semantic-release-toolkit/commit/e5d68f864fecd8f7be5ce97a533bda1ce6568096)), closes [#120](https://github.com/qiwi/semantic-release-toolkit/issues/120) 51 | * introduce semrel-git-utils package ([b35457b](https://github.com/qiwi/semantic-release-toolkit/commit/b35457bf84b5fb4fc600c12defe48416cf3cd92a)), closes [#36](https://github.com/qiwi/semantic-release-toolkit/issues/36) 52 | 53 | 54 | 55 | 56 | 57 | ### Dependencies 58 | 59 | * **@qiwi/semrel-infra:** upgraded to 3.1.0 60 | -------------------------------------------------------------------------------- /packages/git-utils/README.md: -------------------------------------------------------------------------------- 1 | # @qiwi/git-utils 2 | Git utils for CI/CD tools. 3 | 4 | ## Install 5 | ```shell script 6 | yarn add @qiwi/git-utils 7 | ``` 8 | 9 | ## Usage 10 | ```typescript 11 | import { gitTags } from '@qiwi/git-utils' 12 | 13 | const tags = await gitTags({ cwd: '/foo/bar/baz', branch: 'master' }) 14 | ``` 15 | -------------------------------------------------------------------------------- /packages/git-utils/jest.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = {...require('../infra/jest.config.json')} 2 | -------------------------------------------------------------------------------- /packages/git-utils/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@qiwi/git-utils", 3 | "version": "1.1.2", 4 | "publishConfig": { 5 | "access": "public" 6 | }, 7 | "description": "Common git utils", 8 | "keywords": [], 9 | "type": "module", 10 | "module": "./target/es6/index.js", 11 | "main": "./target/es6/index.js", 12 | "exports": "./target/es6/index.js", 13 | "source": "target/ts/index.ts", 14 | "types": "./target/es6/index.d.ts", 15 | "files": [ 16 | "README.md", 17 | "CHANGELOG.md", 18 | "target", 19 | "typings", 20 | "flow-typed" 21 | ], 22 | "scripts": { 23 | "clean": "rimraf target typings flow-typed buildcache coverage docs", 24 | "lint": "eslint 'src/**/*.ts'", 25 | "lint:fix": "yarn lint --fix", 26 | "format": "prettier --write 'src/**/*.ts'", 27 | "test": "concurrently yarn:lint yarn:test:unit yarn:test:depcheck", 28 | "test:unit": "NODE_OPTIONS=--experimental-vm-modules jest --detectOpenHandles --forceExit --runInBand", 29 | "test:depcheck": "npm_config_yes=true npx -p depcheck -p @babel/parser@7.16.4 depcheck --ignores '@jest/globals,@types/*,tslib,eslint-*,prettier-*,@qiwi/semrel-infra,@qiwi/semrel-common,semantic-release' --ignore-patterns 'typings,flow-typed/*'", 30 | "build": "concurrently yarn:build:esnext yarn:build:es6 yarn:build:ts yarn:build:libdef yarn:docs && yarn build:esmfix", 31 | "build:esnext": "mkdirp target/esnext && tsc -p tsconfig.esnext.json", 32 | "build:es6": "mkdirp target/es6 && tsc -p tsconfig.es6.json", 33 | "build:ts": "cpy ./ ../../../target/ts/ --dot --cwd=./src/main/ts/", 34 | "build:libdef": "libdefkit --tsconfig=tsconfig.esnext.json --tsconfig=tsconfig.es6.json --entry=@qiwi/semrel-common/target/es6", 35 | "build:esmfix": "yarn tsc-esm-fix --target=target/es6 --target=target/esnext --ext=.js", 36 | "docs": "typedoc --options ./typedoc.json ./src/main/ts", 37 | "uglify": "for f in $(find target -name '*.js'); do short=${f%.js}; terser -c -m -o $short.js -- $f; done", 38 | "postupdate": "yarn && yarn build && yarn test" 39 | }, 40 | "dependencies": { 41 | "@antongolub/git-root": "^1.5.7", 42 | "@qiwi/semrel-common": "workspace:*", 43 | "@qiwi/substrate": "^1.20.15", 44 | "@types/node": "^18.11.7", 45 | "debug": "^4.3.4", 46 | "execa": "^6.1.0", 47 | "file-url": "^4.0.0", 48 | "nanoid": "^4.0.0", 49 | "tempy": "^3.0.0", 50 | "tslib": "^2.4.0" 51 | }, 52 | "devDependencies": { 53 | "@qiwi/semrel-infra": "workspace:*", 54 | "fs-extra": "^10.1.0" 55 | }, 56 | "repository": { 57 | "type": "git", 58 | "url": "git+https://github.com/qiwi/semantic-release-toolkit.git" 59 | }, 60 | "author": "Anton Golub ", 61 | "license": "MIT", 62 | "bugs": { 63 | "url": "https://github.com/qiwi/semantic-release-toolkit/issues" 64 | }, 65 | "homepage": "https://github.com/qiwi/semantic-release-toolkit/#readme", 66 | "prettier": "prettier-config-qiwi" 67 | } 68 | -------------------------------------------------------------------------------- /packages/git-utils/src/main/ts/git/add.ts: -------------------------------------------------------------------------------- 1 | import { gitExec, IGitCommon, TGitResult } from './exec' 2 | 3 | export interface IGitAdd extends IGitCommon { 4 | file?: string 5 | } 6 | 7 | export const gitAdd = ({ 8 | cwd, 9 | sync, 10 | file = '.', 11 | }: T): TGitResult => 12 | gitExec({ 13 | cwd, 14 | sync: sync as T['sync'], 15 | args: ['add', file], 16 | }) 17 | 18 | export const gitAddAll = ({ 19 | cwd, 20 | sync, 21 | }: T): TGitResult => 22 | gitExec({ 23 | cwd, 24 | sync: sync as T['sync'], 25 | args: ['add', '--all'], 26 | }) 27 | -------------------------------------------------------------------------------- /packages/git-utils/src/main/ts/git/branch.ts: -------------------------------------------------------------------------------- 1 | import { gitExec, IGitCommon, TGitResult } from './exec' 2 | 3 | export interface IGitBranch extends IGitCommon { 4 | branch: string 5 | } 6 | 7 | /** 8 | * Create a branch in a local Git repository. 9 | * 10 | * @param {string} cwd The CWD of the Git repository. 11 | * @param {string} branch Branch name to create. 12 | * @returns {Promise} Promise that resolves when done. 13 | */ 14 | export const gitBranch = ({ 15 | cwd, 16 | sync, 17 | branch, 18 | }: T): TGitResult => { 19 | // Check params. 20 | // check(cwd, 'cwd: absolute') 21 | // check(branch, 'branch: lower') 22 | 23 | return gitExec({ 24 | cwd, 25 | sync: sync as T['sync'], 26 | args: ['branch', branch], 27 | }) 28 | } 29 | -------------------------------------------------------------------------------- /packages/git-utils/src/main/ts/git/checkout.ts: -------------------------------------------------------------------------------- 1 | import { formatFlags } from '@qiwi/semrel-common' 2 | 3 | import { gitExec, IGitCommon, TGitResult } from './exec' 4 | 5 | export interface IGitCheckout extends IGitCommon { 6 | branch: string 7 | b?: boolean 8 | f?: boolean 9 | } 10 | 11 | export const gitCheckout = ({ 12 | cwd, 13 | sync, 14 | branch, 15 | b, 16 | f = !b, 17 | }: T): TGitResult => { 18 | // check(branch, 'branch: kebab') 19 | 20 | const flags = formatFlags({ b, f }) 21 | 22 | return gitExec({ 23 | cwd, 24 | sync: sync as T['sync'], 25 | args: ['checkout', ...flags, branch], 26 | }) 27 | } 28 | -------------------------------------------------------------------------------- /packages/git-utils/src/main/ts/git/commit.ts: -------------------------------------------------------------------------------- 1 | import { formatFlags } from '@qiwi/semrel-common' 2 | 3 | import { exec } from '../misc' 4 | import { gitGetHead } from './etc' 5 | import { gitExec, IGitCommon, TGitResult } from './exec' 6 | 7 | export interface IGitCommit extends IGitCommon { 8 | message: string 9 | all?: boolean 10 | } 11 | 12 | /** 13 | * Get the current HEAD SHA in a local Git repository. 14 | * 15 | * @param {string} cwd The CWD of the Git repository. 16 | * @return {Promise} Promise that resolves to the SHA of the head commit. 17 | */ 18 | export const gitCommit = ({ 19 | cwd, 20 | sync, 21 | message, 22 | all, 23 | }: T): TGitResult => { 24 | // check(cwd, 'cwd: absolute') 25 | // check(message, 'message: string+') 26 | 27 | const flags = formatFlags({ all, message }) 28 | 29 | return exec( 30 | () => 31 | gitExec({ 32 | cwd, 33 | sync, 34 | args: ['commit', ...flags, '--no-gpg-sign', '--allow-empty'], 35 | }), 36 | () => gitGetHead({ cwd, sync: sync as T['sync'] }), 37 | ) 38 | } 39 | 40 | // export const a: string = gitCommit({sync: false, cwd: 'a', message: 'ff'}) 41 | -------------------------------------------------------------------------------- /packages/git-utils/src/main/ts/git/config.ts: -------------------------------------------------------------------------------- 1 | import { gitExec, IGitCommon, TGitResult } from './exec' 2 | 3 | export interface IGitConfigAdd extends IGitCommon { 4 | key: string 5 | value: any 6 | } 7 | 8 | export interface IGitConfigGet extends IGitCommon { 9 | key: string 10 | } 11 | 12 | /** 13 | * Add a Git config setting. 14 | * 15 | * @param {string} cwd The CWD of the Git repository. 16 | * @param {string} name Config name. 17 | * @param {any} value Config value. 18 | * @returns {Promise} Promise that resolves when done. 19 | */ 20 | export const gitConfigAdd = ({ 21 | cwd, 22 | key, 23 | value, 24 | sync, 25 | }: T): TGitResult => { 26 | // check(cwd, 'cwd: absolute') 27 | // check(name, 'name: string+') 28 | 29 | return gitExec({ 30 | cwd, 31 | sync: sync as T['sync'], 32 | args: ['config', '--add', key, value], 33 | }) 34 | } 35 | 36 | export const gitConfig = gitConfigAdd 37 | 38 | /** 39 | * Get a Git config setting. 40 | * 41 | * @param {string} cwd The CWD of the Git repository. 42 | * @param {string} name Config name. 43 | * @returns {Promise} Promise that resolves when done. 44 | */ 45 | export const gitConfigGet = ({ 46 | cwd, 47 | key, 48 | sync, 49 | }: T): TGitResult => { 50 | // check(cwd, 'cwd: absolute') 51 | // check(name, 'name: string+') 52 | 53 | return gitExec({ 54 | cwd, 55 | sync: sync as T['sync'], 56 | args: ['config', key], 57 | }) 58 | } 59 | -------------------------------------------------------------------------------- /packages/git-utils/src/main/ts/git/etc.ts: -------------------------------------------------------------------------------- 1 | import { exec, format } from '../misc' 2 | import { gitExec, IGitCommon, TGitResult } from './exec' 3 | 4 | /** 5 | * Get the current HEAD SHA in a local Git repository. 6 | * 7 | * @param {string} cwd The CWD of the Git repository. 8 | * @return {Promise} Promise that resolves to the SHA of the head commit. 9 | */ 10 | 11 | export const gitGetHead = ({ 12 | cwd, 13 | sync, 14 | }: T): TGitResult => 15 | gitExec({ 16 | cwd, 17 | args: ['rev-parse', 'HEAD'], 18 | sync: sync as T['sync'], 19 | }) 20 | 21 | export interface IGitShowCommitted extends IGitCommon { 22 | hash?: string 23 | } 24 | 25 | export const gitShowCommitted = ({ 26 | cwd, 27 | sync, 28 | hash = 'HEAD', 29 | }: T): TGitResult => 30 | exec( 31 | () => 32 | gitExec({ 33 | cwd, 34 | sync: sync as T['sync'], 35 | args: ['diff-tree', '--no-commit-id', '--name-only', '-r', hash], 36 | }), 37 | (stdout) => format(sync as T['sync'], (stdout as string).split('\n')), 38 | ) 39 | 40 | export const gitStatus = ({ 41 | cwd, 42 | sync, 43 | }: T): TGitResult => 44 | gitExec({ 45 | cwd, 46 | sync: sync as T['sync'], 47 | args: ['status', '--short'], 48 | }) 49 | -------------------------------------------------------------------------------- /packages/git-utils/src/main/ts/git/exec.ts: -------------------------------------------------------------------------------- 1 | import debug, { Debugger } from 'debug' 2 | import { execa, execaSync, SyncOptions } from 'execa' 3 | import { nanoid } from 'nanoid' 4 | 5 | import { ISyncSensitive, SyncGuard } from '../ifaces' 6 | 7 | export interface IGitCommon extends ISyncSensitive { 8 | cwd: string 9 | debug?: Debugger 10 | } 11 | 12 | export type TGitResult< 13 | S extends boolean | undefined, 14 | V = string 15 | > = SyncGuard 16 | 17 | export interface TGitExecContext extends IGitCommon { 18 | args?: any[] 19 | } 20 | 21 | const defaultDebug = debug('git-exec') 22 | 23 | export const gitExec = ( 24 | opts: T, 25 | ): TGitResult => { 26 | const debug = opts.debug || defaultDebug 27 | const { cwd, args = [], sync } = opts 28 | 29 | const execaArgs: [string, readonly string[], SyncOptions] = [ 30 | 'git', 31 | args as string[], 32 | { cwd }, 33 | ] 34 | const gitExecId = nanoid() 35 | const log = (output: T): T => { 36 | debug(`[${gitExecId}]`, output) 37 | return output 38 | } 39 | 40 | log(execaArgs) 41 | 42 | if (sync === true) { 43 | const res = execaSync(...execaArgs) 44 | return (log(res.stdout || res.stderr) as unknown) as TGitResult< 45 | T['sync'] 46 | > 47 | } 48 | 49 | return (execa(...execaArgs).then(({ stdout , stderr}) => 50 | log(stdout.toString() || stderr.toString()), 51 | ) as unknown) as TGitResult 52 | } 53 | -------------------------------------------------------------------------------- /packages/git-utils/src/main/ts/git/fetch.ts: -------------------------------------------------------------------------------- 1 | import { formatFlags } from '@qiwi/semrel-common' 2 | 3 | import { gitExec, IGitCommon, TGitResult } from './exec' 4 | 5 | export interface IGitFetch extends IGitCommon { 6 | remote?: string 7 | branch?: string 8 | depth?: number 9 | } 10 | 11 | export const gitFetchAll = ({ 12 | cwd, 13 | sync, 14 | }: T): TGitResult => 15 | gitExec({ 16 | cwd, 17 | sync: sync as T['sync'], 18 | args: ['fetch', '--all'], 19 | }) 20 | 21 | export const gitFetch = ({ 22 | cwd, 23 | remote = 'origin', 24 | branch, 25 | sync, 26 | depth, 27 | }: T): TGitResult => { 28 | const flags = formatFlags({ depth }) 29 | 30 | return branch 31 | ? gitExec({ 32 | cwd, 33 | sync: sync as T['sync'], 34 | args: ['fetch', ...flags, remote, branch], 35 | }) 36 | : gitFetchAll({cwd, sync: sync as T['sync']}) 37 | } 38 | -------------------------------------------------------------------------------- /packages/git-utils/src/main/ts/git/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Lifted and tweaked from semantic-release because we follow how they test their internals. 3 | * https://github.com/semantic-release/semantic-release/blob/master/test/helpers/git-utils.js 4 | */ 5 | 6 | export * from './add' 7 | export * from './branch' 8 | export * from './checkout' 9 | export * from './config' 10 | export * from './exec' 11 | export * from './etc' 12 | export * from './fetch' 13 | export * from './init' 14 | export * from './commit' 15 | export * from './push' 16 | export * from './rebase' 17 | export * from './remote' 18 | export * from './tag' 19 | export * from './user' 20 | -------------------------------------------------------------------------------- /packages/git-utils/src/main/ts/git/init.ts: -------------------------------------------------------------------------------- 1 | import { gitRoot } from '@antongolub/git-root' 2 | import { formatFlags } from '@qiwi/semrel-common' 3 | import fileUrl from 'file-url' 4 | import { temporaryDirectory } from 'tempy' 5 | 6 | import { exec, format } from '../misc' 7 | import { gitCheckout } from './checkout' 8 | import { gitExec, TGitResult } from './exec' 9 | import { gitPush } from './push' 10 | import { gitRemoteAdd } from './remote' 11 | 12 | export interface IGitInit { 13 | cwd?: string 14 | sync?: boolean 15 | bare?: boolean 16 | } 17 | 18 | export interface IGitInitOrigin extends IGitInit { 19 | cwd: string 20 | branch?: string 21 | } 22 | 23 | export { gitRoot } 24 | 25 | export const gitInit = ({ 26 | cwd = temporaryDirectory(), 27 | sync, 28 | bare, 29 | }: T): TGitResult => 30 | exec( 31 | () => gitRoot(cwd, sync), 32 | (parentGitDir) => { 33 | if (parentGitDir) { 34 | throw new Error( 35 | `${cwd} belongs to repo ${parentGitDir as string} already`, 36 | ) 37 | } 38 | }, 39 | () => { 40 | const flags = formatFlags({ bare }) 41 | 42 | return gitExec({ 43 | cwd: cwd as string, 44 | sync, 45 | args: ['init', ...flags], 46 | }) 47 | }, 48 | () => format(sync as T['sync'], cwd), 49 | ) 50 | 51 | /** 52 | * Init bare Git repository in a temp directory. 53 | * 54 | * @return {Promise} Promise that resolves to string URL of the of the remote origin. 55 | */ 56 | export const gitInitRemote = ({ 57 | cwd = temporaryDirectory(), 58 | sync, 59 | }: T): TGitResult => 60 | exec( 61 | () => gitInit({ cwd, sync, bare: true }), 62 | // Turn remote path into a file URL. 63 | (cwd) => format(sync as T['sync'], fileUrl(cwd)), 64 | ) 65 | 66 | /** 67 | * Create a remote Git repository and set it as the origin for a Git repository. 68 | * _Created in a temp folder._ 69 | * 70 | * @param {string} cwd The cwd to create and set the origin for. 71 | * @param {string} [releaseBranch] Optional branch to be added in case of prerelease is activated for a branch. 72 | * @return {Promise} Promise that resolves to string URL of the of the remote origin. 73 | */ 74 | export const gitInitOrigin = ({ 75 | cwd, 76 | sync, 77 | branch, 78 | }: T): TGitResult => { 79 | // Check params. 80 | // check(cwd, 'cwd: absolute') 81 | 82 | let url: string 83 | 84 | return exec( 85 | // Turn remote path into a file URL. 86 | () => gitInitRemote({ sync }), 87 | (_url) => { 88 | url = _url 89 | }, 90 | () => gitRemoteAdd({ sync, cwd, url }), 91 | () => 92 | // Set up a release branch. Return to master afterwards. 93 | branch && 94 | exec( 95 | // sync as T['sync'], 96 | () => gitCheckout({ cwd, sync, branch, b: true }), 97 | () => gitCheckout({ cwd, sync, branch: 'master' }), 98 | ), 99 | () => gitPush({ cwd, sync, branch }), 100 | () => format(sync as T['sync'], url), 101 | ) 102 | } 103 | 104 | /* (cwd: string, releaseBranch?: string): string => { 105 | // Check params. 106 | check(cwd, 'cwd: absolute') 107 | 108 | // Turn remote path into a file URL. 109 | const url = gitInitRemote() 110 | 111 | // Set origin on local repo. 112 | execaSync('git', ['remote', 'add', 'origin', url], { cwd }) 113 | 114 | // Set up a release branch. Return to master afterwards. 115 | if (releaseBranch) { 116 | execaSync('git', ['checkout', '-b', releaseBranch], { cwd }) 117 | execaSync('git', ['checkout', 'master'], { cwd }) 118 | } 119 | 120 | execaSync('git', ['push', '--all', 'origin'], { cwd }) 121 | 122 | // Return URL for remote. 123 | return url 124 | } */ 125 | -------------------------------------------------------------------------------- /packages/git-utils/src/main/ts/git/push.ts: -------------------------------------------------------------------------------- 1 | import { exec } from '../misc' 2 | import { gitGetHead } from './etc' 3 | import { gitExec, IGitCommon, TGitResult } from './exec' 4 | import { gitFetch } from './fetch' 5 | import { gitRebaseToRemote } from './rebase' 6 | 7 | export interface IGitPush extends IGitCommon { 8 | branch?: string 9 | refspec?: string 10 | remote?: string 11 | } 12 | 13 | export interface IGitPushRebase extends IGitPush { 14 | depth?: number 15 | } 16 | 17 | export const gitPush = ({ 18 | cwd, 19 | sync, 20 | branch, 21 | remote = 'origin', 22 | refspec = `HEAD:refs/heads/${branch || 'master'}`, 23 | }: T): TGitResult => { 24 | // check(cwd, 'cwd: absolute') 25 | // check(remote, 'remote: string') 26 | // check(branch, 'branch: lower') 27 | 28 | const args = branch 29 | ? ['push', '--tags', remote, refspec] 30 | : ['push', '--all', '--follow-tags', remote] 31 | 32 | return exec( 33 | () => 34 | gitExec({ 35 | cwd, 36 | sync, 37 | args, 38 | }), 39 | () => gitGetHead({ cwd, sync: sync as T['sync'] }), 40 | ) 41 | } 42 | 43 | export const gitPushRebase = ({ 44 | cwd, 45 | sync, 46 | branch, 47 | remote = 'origin', 48 | refspec, 49 | }: T): TGitResult => 50 | (sync 51 | ? gitPushRebaseSync({ cwd, sync, branch, remote, refspec}) 52 | : gitPushRebaseAsync({ cwd, sync, branch, remote, refspec })) as TGitResult< 53 | T['sync'] 54 | > 55 | 56 | export const gitPushRebaseAsync = async ({ 57 | cwd, 58 | sync, 59 | branch, 60 | remote = 'origin', 61 | refspec, 62 | depth, 63 | }: T): Promise => { 64 | let retries = 5 65 | 66 | while (retries > 0) { 67 | try { 68 | try { 69 | await gitFetch({ cwd, sync, branch, remote, depth }) 70 | await gitRebaseToRemote({ cwd, sync, branch, remote }) 71 | } catch (e) { 72 | console.warn('rebase failed', e) 73 | } 74 | 75 | return await gitPush({ cwd, sync, branch, remote, refspec }) 76 | } catch (e) { 77 | retries -= 1 78 | console.warn('push failed', 'retries left', retries, e) 79 | } 80 | } 81 | 82 | throw new Error('`gitPushRebase` failed') 83 | } 84 | 85 | export const gitPushRebaseSync = ({ 86 | cwd, 87 | sync, 88 | branch, 89 | remote = 'origin', 90 | refspec, 91 | depth, 92 | }: T): string => { 93 | let retries = 5 94 | 95 | while (retries > 0) { 96 | try { 97 | try { 98 | gitFetch({ cwd, sync, branch, remote, depth }) 99 | gitRebaseToRemote({ cwd, sync, branch, remote }) 100 | } catch (e) { 101 | console.warn('rebase failed', e) 102 | } 103 | 104 | return (gitPush({ cwd, sync, branch, remote, refspec }) as unknown) as string 105 | } catch (e) { 106 | retries -= 1 107 | console.warn('push failed', 'retries left', retries, e) 108 | } 109 | } 110 | 111 | throw new Error('`gitPushRebase` failed') 112 | } 113 | -------------------------------------------------------------------------------- /packages/git-utils/src/main/ts/git/rebase.ts: -------------------------------------------------------------------------------- 1 | import { gitExec, IGitCommon, TGitResult } from './exec' 2 | 3 | export interface IGitRebase extends IGitCommon { 4 | remote?: string 5 | branch?: string 6 | } 7 | 8 | export const gitRebaseToRemote = ({ 9 | cwd, 10 | sync, 11 | remote, 12 | branch, 13 | }: T): TGitResult => 14 | gitExec({ 15 | cwd, 16 | sync: sync as T['sync'], 17 | args: ['rebase', `${remote}/${branch}`], 18 | }) 19 | -------------------------------------------------------------------------------- /packages/git-utils/src/main/ts/git/remote.ts: -------------------------------------------------------------------------------- 1 | import { gitExec, IGitCommon, TGitResult } from './exec' 2 | 3 | export interface IGitRemoteAdd extends IGitCommon { 4 | url: string 5 | remote?: string 6 | } 7 | 8 | export const gitRemoteAdd = ({ 9 | cwd, 10 | sync, 11 | url, 12 | remote = 'origin', 13 | }: T): TGitResult => 14 | gitExec({ 15 | cwd, 16 | sync: sync as T['sync'], 17 | args: ['remote', 'add', remote, url], 18 | }) 19 | 20 | export interface IGitSetRemoteHead extends IGitCommon { 21 | remote?: string 22 | } 23 | 24 | export const gitRemoteSetHead = ({ 25 | cwd, 26 | sync, 27 | remote = 'origin', 28 | }: T): TGitResult => 29 | gitExec({ 30 | cwd, 31 | sync: sync as T['sync'], 32 | args: ['remote', 'set-head', remote, '--auto'], 33 | }) 34 | -------------------------------------------------------------------------------- /packages/git-utils/src/main/ts/git/tag.ts: -------------------------------------------------------------------------------- 1 | import { effect } from '../misc' 2 | import { gitExec, IGitCommon, TGitResult } from './exec' 3 | 4 | export interface IGitTag extends IGitCommon { 5 | tag?: string 6 | hash?: string 7 | } 8 | 9 | /** 10 | * Create a tag on the HEAD commit in a local Git repository. 11 | * 12 | * @param {string} cwd The CWD of the Git repository. 13 | * @param {string} tag The tag name to create. 14 | * @param {string} hash=false SHA for the commit on which to create the tag. If falsy the tag is created on the latest commit. 15 | * @returns {Promise} Promise that resolves when done. 16 | */ 17 | export const gitTag = ({ 18 | cwd, 19 | sync, 20 | tag, 21 | hash, 22 | }: T): TGitResult => { 23 | // Check params. 24 | // check(cwd, 'cwd: absolute') 25 | // check(tag, 'tagName: string+') 26 | // check(hash, 'hash: alphanumeric{40}?') 27 | 28 | const flags = hash ? ['-f', tag, hash] : [tag] 29 | 30 | return gitExec({ 31 | cwd, 32 | sync: sync as T['sync'], 33 | args: ['tag', ...flags], 34 | }) 35 | } 36 | 37 | /** 38 | * Get tag list associated with a commit SHA. 39 | * 40 | * @param {string} cwd The CWD of the Git repository. 41 | * @param {string} hash The commit SHA for which to retrieve the associated tag. 42 | * @return {Promise} The tag associated with the SHA in parameter or `null`. 43 | */ 44 | export const gitGetTags = ({ 45 | cwd, 46 | sync, 47 | hash, 48 | }: T): TGitResult => { 49 | // Check params. 50 | // check(cwd, 'cwd: absolute') 51 | // check(hash, 'hash: alphanumeric{40}') 52 | 53 | return effect( 54 | gitExec({ 55 | cwd, 56 | sync, 57 | args: ['tag', '--merged', hash], 58 | }), 59 | (tags) => (tags ? tags.split('\n') : []), 60 | ) 61 | } 62 | 63 | /** 64 | * Get the first commit SHA tagged `tagName` in a local Git repository. 65 | * 66 | * @param {string} cwd The CWD of the Git repository. 67 | * @param {string} tag Tag name for which to retrieve the commit sha. 68 | * @return {Promise} Promise that resolves to the SHA of the first commit associated with `tagName`. 69 | */ 70 | export const gitGetTagHash = ({ 71 | cwd, 72 | sync, 73 | tag, 74 | }: T): TGitResult => { 75 | // Check params. 76 | // Check params. 77 | // check(cwd, 'cwd: absolute') 78 | // check(tag, 'tag: string+') 79 | 80 | return gitExec({ 81 | cwd, 82 | sync: sync as T['sync'], 83 | args: ['rev-list', '-1', tag], 84 | }) 85 | } 86 | -------------------------------------------------------------------------------- /packages/git-utils/src/main/ts/git/user.ts: -------------------------------------------------------------------------------- 1 | import { exec } from '../misc' 2 | import { gitConfigAdd } from './config' 3 | import { IGitCommon, TGitResult } from './exec' 4 | 5 | export interface IGitSetUser extends IGitCommon { 6 | name: string 7 | email: string 8 | } 9 | 10 | /** 11 | * Set user for git repo. 12 | * 13 | * @param {string} cwd The CWD of the Git repository. 14 | * @param {string} name User name. 15 | * @param {string} email User email. 16 | * @returns {Promise} Promise that resolves when done. 17 | */ 18 | export const gitSetUser = ({ 19 | cwd, 20 | name, 21 | email, 22 | sync, 23 | }: T): TGitResult => { 24 | // check(email, 'email: string+') 25 | // check(name, 'name: string+') 26 | 27 | return exec( 28 | () => 29 | gitConfigAdd({ 30 | cwd, 31 | sync: sync as T['sync'], 32 | key: 'user.name', 33 | value: name, 34 | }), 35 | () => 36 | gitConfigAdd({ 37 | cwd, 38 | sync: sync as T['sync'], 39 | key: 'user.email', 40 | value: email, 41 | }), 42 | ) 43 | } 44 | -------------------------------------------------------------------------------- /packages/git-utils/src/main/ts/ifaces.ts: -------------------------------------------------------------------------------- 1 | import { Extends } from '@qiwi/substrate' 2 | 3 | export interface ISyncSensitive { 4 | sync?: boolean 5 | } 6 | 7 | export type TSyncDirective = boolean | undefined | ISyncSensitive 8 | 9 | export type ParseSync = Extends< 10 | T, 11 | boolean | undefined, 12 | T, 13 | T extends ISyncSensitive ? T['sync'] : never 14 | > 15 | 16 | export type SyncGuard = Extends< 17 | S, 18 | { sync: true } | true, 19 | Extends, never, V>, 20 | Extends, V, Promise> 21 | > 22 | -------------------------------------------------------------------------------- /packages/git-utils/src/main/ts/index.ts: -------------------------------------------------------------------------------- 1 | export * from './git' 2 | export * from './misc' 3 | -------------------------------------------------------------------------------- /packages/git-utils/src/main/ts/misc.ts: -------------------------------------------------------------------------------- 1 | import { GetLength, ICallable, Prev } from '@qiwi/substrate' 2 | import util from 'util' 3 | 4 | import { SyncGuard, TSyncDirective } from './ifaces' 5 | 6 | export type BoolOrEmpty = boolean | undefined 7 | 8 | export const isPromiseLike = (value: any): boolean => 9 | typeof (value as any)?.then === 'function' 10 | 11 | export const execute = ( 12 | ...callbacks: C 13 | ): ReturnType>]> => 14 | callbacks.reduce((prev, cb) => effect(prev, cb)) as ReturnType< 15 | C[Prev>] 16 | > 17 | 18 | export const exec = ( 19 | // sync: S, 20 | ...callbacks: C 21 | ): ReturnType>]> => 22 | callbacks.reduce((prev, cb) => effect(prev, cb), {} as any) // as SyncGuard>]>, S> 23 | 24 | export const effect = ( 25 | value: V, 26 | cb: C, 27 | ): ReturnType => 28 | isPromiseLike(value) ? (value as any)?.then(cb) : cb(value) 29 | 30 | export const format = ( 31 | sync: S, 32 | value: V, 33 | ): SyncGuard => 34 | (sync === true 35 | ? value 36 | : isPromiseLike(value) 37 | ? value 38 | : Promise.resolve(value)) as SyncGuard 39 | 40 | export const extractValue = (value: any): any => 41 | isPromiseLike(value) 42 | ? // For example: "Promise { 'foo' }" 43 | util.inspect(value).slice(11, -3) 44 | : value 45 | 46 | // export const a: SyncGuard = Promise.resolve('a') 47 | -------------------------------------------------------------------------------- /packages/git-utils/src/test/fixtures/basicPackage/inner/file.txt: -------------------------------------------------------------------------------- 1 | contents 2 | -------------------------------------------------------------------------------- /packages/git-utils/src/test/fixtures/basicPackage/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msr-test-basic", 3 | "author": "Dave Houlbrooke =8.3" 9 | }, 10 | "release": { 11 | "plugins": [ 12 | "@semantic-release/commit-analyzer", 13 | "@semantic-release/release-notes-generator" 14 | ], 15 | "noCi": true 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/git-utils/src/test/fixtures/foo/bar/foobar.txt: -------------------------------------------------------------------------------- 1 | foobar 2 | -------------------------------------------------------------------------------- /packages/git-utils/src/test/fixtures/foo/baz/foobaz.txt: -------------------------------------------------------------------------------- 1 | foobaz 2 | -------------------------------------------------------------------------------- /packages/git-utils/src/test/fixtures/foo/foo/foofoo.txt: -------------------------------------------------------------------------------- 1 | foofoo 2 | -------------------------------------------------------------------------------- /packages/git-utils/src/test/ts/it/git.ts: -------------------------------------------------------------------------------- 1 | import { dirname } from 'node:path' 2 | 3 | import fs from 'fs-extra' 4 | import path from 'path' 5 | import { rootTemporaryDirectory,temporaryDirectory } from 'tempy' 6 | import { fileURLToPath } from 'url' 7 | 8 | import { 9 | gitCheckout, 10 | gitConfigAdd, 11 | gitConfigGet, 12 | gitExec, 13 | gitInit, 14 | gitRoot, 15 | gitSetUser, 16 | } from '../../../main/ts' 17 | 18 | const __filename = fileURLToPath(import.meta.url) 19 | const __dirname = dirname(__filename) 20 | const root = path.resolve(__dirname, '../../../../../../') 21 | 22 | describe('git-utils', () => { 23 | describe('gitRoot()', () => { 24 | it('returns the closest .git containing path', async () => { 25 | expect(await gitRoot(__filename)).toBe(root) 26 | }) 27 | 28 | // https://git-scm.com/docs/gitrepository-layout 29 | describe('gitdir ref', () => { 30 | it('handles `gitdir: ref` and returns target path if exists', async () => { 31 | const temp0 = temporaryDirectory() 32 | const temp1 = temporaryDirectory() 33 | const data = `gitdir: ${temp1}.git ` 34 | 35 | await fs.outputFile(path.join(temp0, '.git'), data, { 36 | encoding: 'utf8', 37 | }) 38 | 39 | expect(await gitRoot(temp0)).toBe(temp1) 40 | }) 41 | 42 | it('returns undefined if `gitdir: ref` is unreachable', async () => { 43 | const temp = temporaryDirectory() 44 | const data = `gitdir: /foo/bar/baz.git ` 45 | 46 | await fs.outputFile(path.join(temp, '.git'), data, { encoding: 'utf8' }) 47 | 48 | expect(await gitRoot(temp)).toBeUndefined() 49 | }) 50 | 51 | it('returns undefined if `gitdir: ref` is invalid', async () => { 52 | const temp = temporaryDirectory() 53 | const data = `gitdir: broken-ref-format` 54 | 55 | await fs.outputFile(path.join(temp, '.git'), data, { encoding: 'utf8' }) 56 | 57 | expect(await gitRoot(temp)).toBeUndefined() 58 | }) 59 | }) 60 | 61 | it('returns undefined if `.git` is not found', async () => { 62 | expect(await gitRoot(rootTemporaryDirectory)).toBeUndefined() 63 | }) 64 | }) 65 | 66 | describe('gitConfigAdd() / gitConfigGet()', () => { 67 | it('sets git config value', async () => { 68 | const key = 'user.name' 69 | const value = 'Foo Bar' 70 | const cwd = await gitInit({}) 71 | 72 | await gitConfigAdd({ cwd, key, value }) 73 | 74 | expect(await gitConfigGet({ cwd, key })).toBe(value) 75 | }) 76 | }) 77 | 78 | describe('gitInit()', () => { 79 | const isGitDir = async (cwd: string): Promise => 80 | (await gitRoot(cwd)) === cwd 81 | 82 | it('inits a new git project in temp dir', async () => { 83 | const cwd = await gitInit({}) 84 | 85 | expect(cwd).toEqual(expect.any(String)) 86 | expect(cwd).not.toBe(root) 87 | expect(await isGitDir(cwd)).toBe(true) 88 | }) 89 | 90 | it('inits repo in specified dir', async () => { 91 | const cwd = temporaryDirectory() 92 | const _cwd = await gitInit({ cwd }) 93 | 94 | expect(cwd).toBe(_cwd) 95 | expect(await isGitDir(cwd)).toBe(true) 96 | }) 97 | 98 | it('asserts that cwd does not belong to git repo', async () => { 99 | expect(gitInit({ cwd: __dirname })).rejects.toThrowError( 100 | `${__dirname} belongs to repo ${root} already`, 101 | ) 102 | }) 103 | }) 104 | 105 | describe('gitCheckout()', () => { 106 | it('checkout -b creates a branch', async () => { 107 | const cwd = await gitInit({ cwd: temporaryDirectory() }) 108 | await gitSetUser({ cwd, name: 'Foo Bar', email: 'foo@bar.com' }) 109 | 110 | await fs.writeFile(path.resolve(cwd, 'test.txt'), 'test', { 111 | encoding: 'utf-8', 112 | }) 113 | 114 | await gitExec({ cwd, args: ['add', '.'] }) 115 | await gitExec({ cwd, args: ['commit', '-a', '-m', 'initial'] }) 116 | 117 | await gitCheckout({ cwd, b: true, branch: 'foobar' }) 118 | 119 | const branches = await gitExec({ cwd, args: ['branch', '-a'] }) 120 | 121 | expect(branches.includes('foobar')).toBeTruthy() 122 | }) 123 | }) 124 | }) 125 | -------------------------------------------------------------------------------- /packages/git-utils/src/test/ts/unit/git.ts: -------------------------------------------------------------------------------- 1 | // https://github.com/facebook/jest/pull/11818/files#diff-7a98537bdfc98f8a0321f1e556bc226e1eb013e30c266962a788c77df4289a61R181 2 | import { jest } from '@jest/globals' 3 | import { ICallable } from '@qiwi/substrate' 4 | import { temporaryDirectory } from 'tempy' 5 | 6 | const fakeExec = (..._args: any[]) => ({ stdout: 'output' }) // eslint-disable-line 7 | const execa = jest.fn(() => Promise.resolve(fakeExec())) 8 | const execaSync = jest.fn(fakeExec) 9 | 10 | jest.unstable_mockModule('execa', () => ({ 11 | __esModule: true, 12 | execa, 13 | execaSync 14 | })) 15 | 16 | const { 17 | gitAdd, 18 | gitAddAll, 19 | gitBranch, 20 | gitCheckout, 21 | gitCommit, 22 | gitConfigAdd, 23 | gitConfigGet, 24 | gitFetch, 25 | gitFetchAll, 26 | gitGetHead, 27 | gitGetTagHash, 28 | gitGetTags, 29 | gitInit, 30 | gitInitOrigin, 31 | gitInitRemote, 32 | gitPush, 33 | gitPushRebase, 34 | gitRebaseToRemote, 35 | gitRemoteAdd, 36 | gitRemoteSetHead, 37 | gitSetUser, 38 | gitShowCommitted, 39 | gitStatus, 40 | gitTag, 41 | } = await import('../../../main/ts') 42 | 43 | describe('git-utils', () => { 44 | afterAll(jest.restoreAllMocks) 45 | afterEach(jest.clearAllMocks) 46 | 47 | const cwd = temporaryDirectory() 48 | const cases: [ICallable, Record, any[][], any?][] = [ 49 | [gitInit, { cwd }, [['git', ['init'], { cwd }]], cwd], 50 | [ 51 | gitInitRemote, 52 | { cwd }, 53 | [['git', ['init', '--bare'], { cwd }]], 54 | expect.any(String), 55 | ], 56 | [ 57 | gitInitOrigin, 58 | { cwd, branch: 'foo' }, 59 | [['git', ['push', '--tags', 'origin', 'HEAD:refs/heads/foo'], { cwd }]], 60 | expect.any(String), 61 | ], 62 | [ 63 | gitCheckout, 64 | { cwd, b: true, branch: 'foobar' }, 65 | [['git', ['checkout', '-b', 'foobar'], { cwd }]], 66 | 'output', 67 | ], 68 | [ 69 | gitConfigAdd, 70 | { cwd, key: 'user.name', value: 'Foo Bar' }, 71 | [['git', ['config', '--add', 'user.name', 'Foo Bar'], { cwd }]], 72 | 'output', 73 | ], 74 | [ 75 | gitConfigGet, 76 | { cwd, key: 'user.name' }, 77 | [['git', ['config', 'user.name'], { cwd }]], 78 | 'output', 79 | ], 80 | [ 81 | gitRemoteAdd, 82 | { cwd, remote: 'qiwi', url: 'git@gh.com:qiwi/foo.git' }, 83 | [['git', ['remote', 'add', 'qiwi', 'git@gh.com:qiwi/foo.git'], { cwd }]], 84 | 'output', 85 | ], 86 | [gitFetchAll, { cwd }, [['git', ['fetch', '--all'], { cwd }]], 'output'], 87 | [ 88 | gitFetch, 89 | { cwd, origin: 'qiwi', branch: 'master' }, 90 | [['git', ['fetch', 'origin', 'master'], { cwd }]], 91 | 'output', 92 | ], 93 | [gitFetch, { cwd }, [['git', ['fetch', '--all'], { cwd }]], 'output'], 94 | [ 95 | gitRemoteSetHead, 96 | { cwd, remote: 'qiwi' }, 97 | [['git', ['remote', 'set-head', 'qiwi', '--auto'], { cwd }]], 98 | 'output', 99 | ], 100 | [ 101 | gitAdd, 102 | { cwd, file: 'qiwi*' }, 103 | [['git', ['add', 'qiwi*'], { cwd }]], 104 | 'output', 105 | ], 106 | [gitAddAll, { cwd }, [['git', ['add', '--all'], { cwd }]], 'output'], 107 | [gitTag, { cwd, tag: 'foo' }, [['git', ['tag', 'foo'], { cwd }]], 'output'], 108 | [ 109 | gitTag, 110 | { cwd, tag: 'foo', hash: 'bar' }, 111 | [['git', ['tag', '-f', 'foo', 'bar'], { cwd }]], 112 | 'output', 113 | ], 114 | [ 115 | gitGetTags, 116 | { cwd, hash: 'bar' }, 117 | [['git', ['tag', '--merged', 'bar'], { cwd }]], 118 | ['output'], 119 | ], 120 | [ 121 | gitGetTagHash, 122 | { cwd, tag: 'foo' }, 123 | [['git', ['rev-list', '-1', 'foo'], { cwd }]], 124 | 'output', 125 | ], 126 | [ 127 | gitBranch, 128 | { cwd, branch: 'foo' }, 129 | [['git', ['branch', 'foo'], { cwd }]], 130 | 'output', 131 | ], 132 | [gitGetHead, { cwd }, [['git', ['rev-parse', 'HEAD'], { cwd }]], 'output'], 133 | [ 134 | gitCommit, 135 | { cwd, message: 'foo' }, 136 | [['git', ['commit', '--message', 'foo', '--no-gpg-sign', '--allow-empty'], { cwd }]], 137 | 'output', 138 | ], 139 | [ 140 | gitCommit, 141 | { cwd, message: 'foo', all: true }, 142 | [ 143 | [ 144 | 'git', 145 | ['commit', '--all', '--message', 'foo', '--no-gpg-sign', '--allow-empty'], 146 | { cwd }, 147 | ], 148 | ], 149 | 'output', 150 | ], 151 | [ 152 | gitPush, 153 | { cwd, branch: 'metabranch', remote: 'qiwi' }, 154 | [ 155 | [ 156 | 'git', 157 | ['push', '--tags', 'qiwi', 'HEAD:refs/heads/metabranch'], 158 | { cwd }, 159 | ], 160 | ], 161 | 'output', 162 | ], 163 | [ 164 | gitRebaseToRemote, 165 | { cwd, branch: 'metabranch', remote: 'qiwi' }, 166 | [['git', ['rebase', 'qiwi/metabranch'], { cwd }]], 167 | 'output', 168 | ], 169 | [ 170 | gitPushRebase, 171 | { cwd, branch: 'metabranch', remote: 'qiwi' }, 172 | [ 173 | ['git', ['fetch', 'qiwi', 'metabranch'], { cwd }], 174 | ['git', ['rebase', 'qiwi/metabranch'], { cwd }], 175 | [ 176 | 'git', 177 | ['push', '--tags', 'qiwi', 'HEAD:refs/heads/metabranch'], 178 | { cwd }, 179 | ], 180 | ['git', ['rev-parse', 'HEAD'], { cwd }], 181 | ], 182 | 'output', 183 | ], 184 | [gitStatus, { cwd }, [['git', ['status', '--short'], { cwd }]], 'output'], 185 | [ 186 | gitShowCommitted, 187 | { cwd, hash: 'foo' }, 188 | [ 189 | [ 190 | 'git', 191 | ['diff-tree', '--no-commit-id', '--name-only', '-r', 'foo'], 192 | { cwd }, 193 | ], 194 | ], 195 | ['output'], 196 | ], 197 | [ 198 | gitSetUser, 199 | { cwd, name: 'Foo Bar', email: 'foo@bar.com' }, 200 | [ 201 | ['git', ['config', '--add', 'user.name', 'Foo Bar'], { cwd }], 202 | ['git', ['config', '--add', 'user.email', 'foo@bar.com'], { cwd }], 203 | ], 204 | 'output', 205 | ], 206 | ] 207 | 208 | cases.forEach(([fn, ctx, argsOfArgs, result]) => { 209 | it(`${fn.name}`, async () => { 210 | await fn(ctx) 211 | expect(fn({ ...ctx, sync: true })).toEqual(result) 212 | 213 | argsOfArgs.forEach((args) => { 214 | expect(execa).toHaveBeenCalledWith(...args) 215 | expect(execaSync).toHaveBeenCalledWith(...args) 216 | }) 217 | }) 218 | }) 219 | }) 220 | -------------------------------------------------------------------------------- /packages/git-utils/src/test/ts/unit/index.ts: -------------------------------------------------------------------------------- 1 | import { gitStatus } from '../../../main/ts' 2 | 3 | describe('index', () => { 4 | it('properly exports its inners', () => { 5 | const gitMethods = [gitStatus] 6 | 7 | gitMethods.forEach((method) => expect(method).toEqual(expect.any(Function))) 8 | }) 9 | }) 10 | -------------------------------------------------------------------------------- /packages/git-utils/tsconfig.es6.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "target": "es6", 5 | "module": "es6", 6 | "outDir": "target/es6" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/git-utils/tsconfig.esnext.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "target": "esnext", 5 | "module": "esnext", 6 | "outDir": "target/esnext" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/git-utils/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../packages/infra/tsconfig.compiler.json", 3 | "compilerOptions": { 4 | "rootDir": "./src/main/ts/", 5 | "baseUrl": "./src/main/ts/", 6 | "tsBuildInfoFile": "./buildcache/.tsbuildinfo", 7 | }, 8 | "references": [], 9 | "include": [ 10 | "src/main/**/*" 11 | ], 12 | "exclude": [ 13 | "node_modules" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /packages/git-utils/tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "target": "esnext", 5 | "module": "esnext", 6 | "importHelpers": true, 7 | "noEmitHelpers": true, 8 | "esModuleInterop": true 9 | }, 10 | "include": [ 11 | "src/**/*" 12 | ], 13 | "exclude": [ 14 | "node_modules" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /packages/git-utils/typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@qiwi/git-utils", 3 | "out": "./docs", 4 | "exclude": ["src/test", "**/node_modules/**", "paralleljs"], 5 | "externalPattern": ["**/node_modules/**"], 6 | "excludePrivate": false, 7 | "hideGenerator": true, 8 | "readme": "README.md", 9 | "theme": "default", 10 | "tsconfig": "./tsconfig.es6.json" 11 | } 12 | -------------------------------------------------------------------------------- /packages/infra/README.md: -------------------------------------------------------------------------------- 1 | # @qiwi/semrel-infra 2 | 3 | Infra package: build tools, configs and other shared assets 4 | -------------------------------------------------------------------------------- /packages/infra/index.js: -------------------------------------------------------------------------------- 1 | export const infra = '@qiwi/semrel-infra' 2 | -------------------------------------------------------------------------------- /packages/infra/jest.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "testEnvironment": "node", 3 | "transform": { 4 | "^.+\\.tsx?$": ["ts-jest", { 5 | "useESM": true, 6 | "tsconfig": "/tsconfig.test.json" 7 | }] 8 | }, 9 | "extensionsToTreatAsEsm": [".ts", ".esm", ".esm.js"], 10 | "transformIgnorePatterns": [], 11 | "collectCoverage": true, 12 | "collectCoverageFrom": [ 13 | "/src/main/**/*.(j|t)s" 14 | ], 15 | "testMatch": [ 16 | "/src/test/js/**/*.js", 17 | "/src/test/cjs/**/*.(c)?js", 18 | "/src/test/mjs/**/*.(m)?js", 19 | "/src/test/ts/**/*.ts" 20 | ], 21 | "testPathIgnorePatterns": [ 22 | "/node_modules/" 23 | ], 24 | "moduleFileExtensions": [ 25 | "ts", 26 | "tsx", 27 | "js", 28 | "jsx", 29 | "json", 30 | "node", 31 | "mjs", 32 | "cjs" 33 | ], 34 | "preset": "ts-jest" 35 | } 36 | -------------------------------------------------------------------------------- /packages/infra/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@qiwi/semrel-infra", 3 | "version": "3.2.4", 4 | "publishConfig": { 5 | "access": "public" 6 | }, 7 | "description": "QIWI semrel infra", 8 | "files": [], 9 | "scripts": { 10 | "postupdate": "yarn" 11 | }, 12 | "type": "module", 13 | "bin": { 14 | "concurrently": "./../../node_modules/.bin/concurrently", 15 | "cpy": "./../../node_modules/.bin/cpy", 16 | "depcheck": "./../../node_modules/.bin/depcheck", 17 | "eslint": "./../../node_modules/.bin/eslint", 18 | "jest": "./../../node_modules/.bin/jest", 19 | "libdefkit": "./../../node_modules/.bin/libdefkit", 20 | "mkdirp": "./../../node_modules/.bin/mkdirp", 21 | "prettier": "./../../node_modules/.bin/prettier", 22 | "rimraf": "./../../node_modules/.bin/rimraf", 23 | "snazzy": "./../../node_modules/.bin/snazzy", 24 | "terser": "./../../node_modules/.bin/terser", 25 | "tsc-esm-fix": "./../../node_modules/.bin/tsc-esm-fix", 26 | "typedoc": "./../../node_modules/.bin/typedoc" 27 | }, 28 | "devDependencies": { 29 | "@jest/globals": "^29.2.2", 30 | "@qiwi/libdefkit": "^5.0.0", 31 | "@swissquote/crafty-preset-jest": "^1.20.0", 32 | "@types/jest": "^29.2.0", 33 | "concurrently": "^8.0.0", 34 | "cpy-cli": "^4.2.0", 35 | "depcheck": "^1.4.3", 36 | "eslint": "^8.26.0", 37 | "eslint-config-prettier": "^8.5.0", 38 | "eslint-config-qiwi": "^1.17.8", 39 | "jest": "^29.2.2", 40 | "mkdirp": "^1.0.4", 41 | "prettier": "^2.7.1", 42 | "prettier-config-qiwi": "^2.0.0", 43 | "rimraf": "^3.0.2", 44 | "snazzy": "^9.0.0", 45 | "terser": "^5.15.1", 46 | "ts-jest": "^29.0.3", 47 | "tsc-esm-fix": "^2.20.5", 48 | "typedoc": "^0.24.0", 49 | "typescript": "^4.8.4" 50 | }, 51 | "repository": { 52 | "type": "git", 53 | "url": "git+https://github.com/qiwi/semantic-release-toolkit.git" 54 | }, 55 | "author": "Anton Golub ", 56 | "license": "MIT", 57 | "bugs": { 58 | "url": "https://github.com/qiwi/semantic-release-toolkit/issues" 59 | }, 60 | "homepage": "https://github.com/qiwi/semantic-release-toolkit/#readme" 61 | } 62 | -------------------------------------------------------------------------------- /packages/infra/tsconfig.compiler.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["esnext"], 4 | "noImplicitAny": true, 5 | "noEmitHelpers": true, 6 | "noUnusedLocals": true, 7 | "noUnusedParameters": true, 8 | "skipLibCheck": false, 9 | "resolveJsonModule": true, 10 | "strictNullChecks": true, 11 | "experimentalDecorators": true, 12 | "emitDecoratorMetadata": true, 13 | "downlevelIteration": true, 14 | "forceConsistentCasingInFileNames": true, 15 | 16 | "target": "esnext", 17 | "module": "esnext", 18 | "moduleResolution": "node", 19 | "declaration": true, 20 | "declarationMap": true, 21 | "sourceMap": true, 22 | "strict": true, 23 | 24 | "esModuleInterop": true, 25 | "allowSyntheticDefaultImports": true, 26 | "importHelpers": true, 27 | "incremental": true, 28 | "composite": true, 29 | }, 30 | "include": [ 31 | "src/main/**/*" 32 | ], 33 | "exclude": [ 34 | "node_modules" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /packages/metabranch/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | 'eslint-config-qiwi', 4 | 'prettier', 5 | ], 6 | overrides: [ 7 | { 8 | files: ['./src/test/**/*.ts'], 9 | rules: { 10 | 'unicorn/consistent-function-scoping': 'off', 11 | 'sonarjs/no-duplicate-string': 'off', 12 | '@typescript-eslint/no-var-requires': 'off', 13 | } 14 | } 15 | ] 16 | }; 17 | -------------------------------------------------------------------------------- /packages/metabranch/README.md: -------------------------------------------------------------------------------- 1 | # @qiwi/semrel-metabranch 2 | [Semrel](https://github.com/semantic-release/semantic-release) plugin for two-way data sync with any remote branch on any release step. 3 | 4 | 5 | | Step | Description | 6 | |--------------------|-------------| 7 | | `verifyConditions` | Performs actions as declared in step options. | 8 | | `analyzeCommits` | As prev. | 9 | | `verifyRelease` | ... | 10 | | `generateNotes` | ... | 11 | | `prepare` | ... | 12 | | `publish` | ... | 13 | | `addChannel` | ... | 14 | | `success` | ... | 15 | | `fail` | ... | 16 | 17 | ## Install 18 | ```shell script 19 | yarn add @qiwi/semrel-metabranch -D 20 | ``` 21 | 22 | ## Usage 23 | As a part of plugin declaration: 24 | ```json 25 | // .release.rc 26 | { 27 | "plugins": [[ 28 | "@qiwi/semrel-metabranch", 29 | { 30 | "verify": { 31 | "action": "fetch", 32 | "branch": "metabranch", 33 | "from": "foo", 34 | "to": "bar" 35 | } 36 | } 37 | ]] 38 | } 39 | ``` 40 | Action declared in release step: 41 | ```json 42 | { 43 | "publish": [[ 44 | "@qiwi/semrel-metabranch", 45 | { 46 | "action": "push", 47 | "branch": "metabranch", 48 | "from": "foo/**/*.txt", 49 | "to": "bar", 50 | "message": "commit message" 51 | } 52 | ]] 53 | } 54 | ``` 55 | 56 | ### GitHub Pages docs pushing example 57 | ```js 58 | module.exports = { 59 | debug: true, 60 | branch: 'master', 61 | plugins: [ 62 | [ 63 | '@qiwi/semrel-metabranch', 64 | { 65 | publish: { 66 | action: 'push', 67 | branch: 'gh-pages', 68 | from: './docs', 69 | to: '.', 70 | message: 'update docs ${nextRelease.gitTag}' 71 | } 72 | } 73 | ], 74 | ... 75 | ] 76 | } 77 | ``` 78 | 79 | ### Configuration 80 | ##### Environment variables 81 | 82 | | Variable | Description | 83 | |------------------------------| --------------------------------------------------------- | 84 | | `GH_TOKEN` or `GITHUB_TOKEN` | **Required.** The token used to authenticate with GitHub. | 85 | 86 | ##### Options 87 | 88 | | Option | Description | Default | 89 | |-----------------|------------------------| --------| 90 | | `action` | Action to perform: `fetch`/`push` | 91 | | `branch` | Branch to push | `metabranch` | 92 | | `message` | Commit message powered by lodash.template: `docs <%= nextRelease.gitTag %>` | `update meta` | 93 | | `from` | Source glob pattern | `.` (root) | 94 | | `to` | Destination directory | `.` (root) | 95 | 96 | 97 | ## API 98 | ### TActionOptions 99 | ```typescript 100 | export type TBaseActionOptions = { 101 | branch: string 102 | from: string | string[] 103 | to: string 104 | message: string 105 | } 106 | 107 | export type TActionOptionsNormalized = TBaseActionOptions & { 108 | repo: string 109 | cwd: string 110 | temp: string 111 | } 112 | 113 | export type TActionType = 'fetch' | 'push' 114 | 115 | export type TActionOptions = Partial & { 116 | repo: string 117 | } 118 | 119 | export type TPluginOptions = Partial & { 120 | action: TActionType 121 | } 122 | ``` 123 | 124 | ### Defaults 125 | ```typescript 126 | export const branch = 'metabranch' 127 | export const from = '.' 128 | export const to = '.' 129 | export const message = 'update meta' 130 | 131 | export const defaults = { 132 | branch, 133 | from, 134 | to, 135 | message, 136 | } 137 | ``` 138 | 139 | # License 140 | MIT 141 | -------------------------------------------------------------------------------- /packages/metabranch/jest.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = {...require('../infra/jest.config.json')} 2 | -------------------------------------------------------------------------------- /packages/metabranch/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@qiwi/semrel-metabranch", 3 | "version": "3.1.2", 4 | "publishConfig": { 5 | "access": "public" 6 | }, 7 | "description": "Semrel plugin for two-way data sync with remote branch", 8 | "keywords": [], 9 | "exports": { 10 | ".": { 11 | "module": "./target/exports/es6.mjs", 12 | "import": "./target/exports/es6.mjs", 13 | "require": "./target/exports/es5.cjs" 14 | } 15 | }, 16 | "module": "./target/exports/es6.mjs", 17 | "source": "./target/ts/index.ts", 18 | "types": "./target/es6/index.d.ts", 19 | "files": [ 20 | "README.md", 21 | "CHANGELOG.md", 22 | "target", 23 | "typings", 24 | "flow-typed" 25 | ], 26 | "scripts": { 27 | "clean": "rimraf target typings flow-typed buildcache coverage docs", 28 | "lint": "eslint 'src/**/*.ts'", 29 | "lint:fix": "yarn lint --fix", 30 | "format": "prettier --write 'src/**/*.ts'", 31 | "test": "concurrently yarn:lint yarn:test:unit yarn:test:depcheck", 32 | "test:unit": "NODE_OPTIONS=--experimental-vm-modules jest --detectOpenHandles --forceExit --runInBand", 33 | "test:depcheck": "npm_config_yes=true npx -p depcheck -p @babel/parser@7.16.4 depcheck --ignores '@qiwi/esm,@jest/globals,@types/*,tslib,eslint-*,prettier-*,@qiwi/semrel-infra,@qiwi/semrel-common' --ignore-patterns 'typings,flow-typed/*'", 34 | "build": "concurrently yarn:build:es5 yarn:build:es6 yarn:build:exports yarn:build:ts yarn:build:esnext yarn:build:libdef yarn:docs && yarn build:esmfix", 35 | "build:esnext": "mkdirp target/esnext && tsc -p tsconfig.esnext.json", 36 | "build:es5": "mkdirp target/es5 && tsc -p tsconfig.es5.json", 37 | "build:es6": "mkdirp target/es6 && tsc -p tsconfig.es6.json", 38 | "build:ts": "cpy ./ ../../../target/ts/ --dot --cwd=./src/main/ts/", 39 | "build:esmfix": "yarn tsc-esm-fix --target=target/es6 --target=target/esnext --dirnameVar=false --ext=.mjs", 40 | "build:exports": "cpy src/main/exports/ target/exports/ --flat", 41 | "build:libdef": "libdefkit --tsconfig=tsconfig.es5.json --tsconfig=tsconfig.es6.json --tsconfig=tsconfig.esnext.json", 42 | "docs": "typedoc --options ./typedoc.json ./src/main/ts", 43 | "uglify": "for f in $(find target -name '*.js'); do short=${f%.js}; terser -c -m -o $short.js -- $f; done", 44 | "postupdate": "yarn && yarn build && yarn test" 45 | }, 46 | "dependencies": { 47 | "@qiwi/esm": "^1.1.8", 48 | "@qiwi/git-utils": "workspace:*", 49 | "@qiwi/semrel-common": "workspace:*", 50 | "@qiwi/semrel-plugin-creator": "workspace:*", 51 | "@types/node": "^18.11.7", 52 | "@types/semantic-release": "^17.2.4", 53 | "execa": "^6.1.0", 54 | "fs-extra": "^10.1.0", 55 | "globby": "^13.1.2", 56 | "tempy": "^3.0.0", 57 | "tslib": "^2.4.0" 58 | }, 59 | "devDependencies": { 60 | "@qiwi/semrel-infra": "workspace:*", 61 | "@qiwi/semrel-testing-suite": "workspace:*", 62 | "resolve-from": "^5.0.0", 63 | "semantic-release": "^19.0.5" 64 | }, 65 | "repository": { 66 | "type": "git", 67 | "url": "git+https://github.com/qiwi/semantic-release-toolkit.git" 68 | }, 69 | "author": "Anton Golub ", 70 | "license": "MIT", 71 | "bugs": { 72 | "url": "https://github.com/qiwi/semantic-release-toolkit/issues" 73 | }, 74 | "homepage": "https://github.com/qiwi/semantic-release-toolkit/#readme", 75 | "prettier": "prettier-config-qiwi", 76 | "main": "./target/exports/es5.cjs" 77 | } 78 | -------------------------------------------------------------------------------- /packages/metabranch/src/main/exports/es5.cjs: -------------------------------------------------------------------------------- 1 | require = require('@qiwi/esm')(module, { 2 | mode: 'all', 3 | cjs: { 4 | cache:false, 5 | esModule:true, 6 | extensions:true, 7 | namedExports:true 8 | }, 9 | force: false, 10 | cache: false, 11 | await: false, 12 | sourceMap: false, 13 | }) 14 | 15 | module.exports = require('../es5/index.js') 16 | -------------------------------------------------------------------------------- /packages/metabranch/src/main/exports/es6.mjs: -------------------------------------------------------------------------------- 1 | export * from '../es6/index.mjs' 2 | -------------------------------------------------------------------------------- /packages/metabranch/src/main/ts/actions.ts: -------------------------------------------------------------------------------- 1 | import { 2 | gitAddAll, 3 | gitCheckout, 4 | gitCommit, 5 | gitFetch, 6 | gitInit, 7 | gitPushRebase, 8 | gitRemoteAdd, 9 | gitRemoteSetHead, 10 | gitSetUser, 11 | gitShowCommitted, 12 | gitStatus, 13 | } from '@qiwi/git-utils' 14 | import { Debugger } from '@qiwi/semrel-plugin-creator' 15 | import fs from 'fs-extra' 16 | import { globby } from 'globby' 17 | import path from 'path' 18 | 19 | import { TActionOptionsNormalized, TActionType, TUserInfo } from './interface' 20 | 21 | export const prepareTempRepo = async ( 22 | cwd: string, 23 | repo: string, 24 | branch: string, 25 | { email, name }: TUserInfo, 26 | ): Promise => { 27 | await gitInit({ cwd }) 28 | await gitSetUser({ cwd, name, email }) 29 | await gitRemoteAdd({ cwd, url: repo, remote: 'origin' }) 30 | 31 | try { 32 | await gitFetch({ cwd, remote: 'origin', branch }) 33 | await gitCheckout({ cwd, branch: `origin/${branch}` }) 34 | } catch { 35 | await gitFetch({ cwd, remote: 'origin' }) 36 | await gitRemoteSetHead({ cwd, remote: 'origin' }) 37 | } 38 | 39 | return cwd 40 | } 41 | 42 | type TSyncOptions = { 43 | from: string | string[] 44 | to: string 45 | baseFrom: string 46 | baseTo: string 47 | debug: Debugger 48 | } 49 | 50 | const synchronize = async ({ 51 | from, 52 | to, 53 | baseFrom, 54 | baseTo, 55 | debug, 56 | }: TSyncOptions): Promise => { 57 | const copy = (src: string, dest: string) => { 58 | debug('copy', 'from=', src, 'to=', dest) 59 | return fs.copy(src, dest) 60 | } 61 | const entries: string[] = Array.isArray(from) ? from : [from] 62 | const patterns: string[] = [] 63 | const dirs: string[] = [] 64 | 65 | await Promise.all( 66 | entries.map(async (entry: string) => { 67 | const entryAbs = path.resolve(baseFrom, entry) 68 | 69 | try { 70 | if ((await fs.lstat(entryAbs))?.isDirectory()) { 71 | dirs.push(entryAbs) 72 | 73 | return 74 | } 75 | } catch {} 76 | 77 | patterns.push(entry) 78 | }), 79 | ) 80 | 81 | await globby(patterns, { cwd: baseFrom, absolute: true }).then((files) => 82 | Promise.all([ 83 | ...files.map((file) => 84 | copy(file, path.resolve(baseTo, to, path.relative(baseFrom, file))), 85 | ), 86 | ...dirs.map((dir) => copy(dir, path.resolve(baseTo, to))), 87 | ]), 88 | ) 89 | } 90 | 91 | export const fetch = async (opts: TActionOptionsNormalized): Promise => { 92 | const { branch, from, to, cwd, temp, repo, debug, user } = opts 93 | 94 | await prepareTempRepo(temp, repo, branch, user) 95 | 96 | await synchronize({ from, to, baseFrom: temp, baseTo: cwd, debug }) 97 | } 98 | 99 | export const push = async (opts: TActionOptionsNormalized): Promise => { 100 | const { branch, from, to, cwd, temp, repo, message, debug, user } = opts 101 | 102 | await prepareTempRepo(temp, repo, branch, user) 103 | 104 | await synchronize({ from, to, baseFrom: cwd, baseTo: temp, debug }) 105 | 106 | await gitAddAll({ cwd: temp }) 107 | 108 | const status = await gitStatus({ cwd: temp }) 109 | debug('status=', status) 110 | 111 | if (!status) { 112 | // debug('contents=', fs.readdirSync(temp)) 113 | return '' 114 | } 115 | 116 | await gitCommit({ cwd: temp, message }) 117 | 118 | const commitId = await gitPushRebase({ cwd: temp, remote: 'origin', branch }) 119 | const committedFiles = await gitShowCommitted({ cwd: temp, hash: commitId }) 120 | 121 | debug('commitId=', commitId, 'committedFiles=', committedFiles.join(', ')) 122 | 123 | return commitId 124 | } 125 | 126 | export const perform = async ( 127 | action: TActionType, 128 | options: TActionOptionsNormalized, 129 | ): Promise => { 130 | if (action === 'push') { 131 | return push(options) 132 | } 133 | 134 | if (action === 'fetch') { 135 | return fetch(options) 136 | } 137 | 138 | throw new Error( 139 | `[metabranch] unsupported action '${action}'. Allowed values: 'fetch' and 'push'`, 140 | ) 141 | } 142 | -------------------------------------------------------------------------------- /packages/metabranch/src/main/ts/index.ts: -------------------------------------------------------------------------------- 1 | import { plugin } from './plugin' 2 | 3 | const { 4 | verifyConditions, 5 | analyzeCommits, 6 | verifyRelease, 7 | generateNotes, 8 | prepare, 9 | publish, 10 | addChannel, 11 | success, 12 | fail, 13 | } = plugin 14 | 15 | export { 16 | verifyConditions, 17 | analyzeCommits, 18 | verifyRelease, 19 | generateNotes, 20 | prepare, 21 | publish, 22 | addChannel, 23 | success, 24 | fail, 25 | } 26 | 27 | export * from './actions' 28 | export * from './interface' 29 | export * from './plugin' 30 | export * from './options' 31 | 32 | export default plugin 33 | -------------------------------------------------------------------------------- /packages/metabranch/src/main/ts/interface.ts: -------------------------------------------------------------------------------- 1 | import { Debugger } from '@qiwi/semrel-plugin-creator' 2 | 3 | export type TBaseActionOptions = { 4 | branch: string 5 | from: string | string[] 6 | to: string 7 | message: string 8 | } 9 | 10 | export type TUserInfo = { 11 | name: string 12 | email: string 13 | } 14 | 15 | export type TActionOptionsNormalized = TBaseActionOptions & { 16 | debug: Debugger 17 | repo: string 18 | cwd: string 19 | temp: string 20 | user: TUserInfo 21 | } 22 | 23 | export type TActionType = 'fetch' | 'push' 24 | 25 | export type TActionOptions = Partial & { 26 | debug: Debugger 27 | repo: string 28 | user: TUserInfo 29 | } 30 | 31 | export type TPluginOptions = Partial & { 32 | action: TActionType 33 | } 34 | -------------------------------------------------------------------------------- /packages/metabranch/src/main/ts/options.ts: -------------------------------------------------------------------------------- 1 | import { temporaryDirectory } from 'tempy' 2 | 3 | import { TActionOptions, TActionOptionsNormalized } from './interface' 4 | 5 | export const branch = 'metabranch' 6 | export const from = '.' 7 | export const to = '.' 8 | export const message = 'update meta' 9 | 10 | export const defaults = { 11 | branch, 12 | from, 13 | to, 14 | message, 15 | } 16 | 17 | export const normalizeOptions = ({ 18 | branch = defaults.branch, 19 | from = defaults.from, 20 | to = defaults.to, 21 | message = defaults.message, 22 | cwd = process.cwd(), 23 | temp = temporaryDirectory(), 24 | repo, 25 | debug, 26 | user, 27 | }: TActionOptions): TActionOptionsNormalized => ({ 28 | branch, 29 | from, 30 | to, 31 | message, 32 | cwd, 33 | temp, 34 | repo, 35 | debug, 36 | user, 37 | }) 38 | -------------------------------------------------------------------------------- /packages/metabranch/src/main/ts/plugin.ts: -------------------------------------------------------------------------------- 1 | import { tpl } from '@qiwi/semrel-common' 2 | import { createPlugin } from '@qiwi/semrel-plugin-creator' 3 | 4 | import { perform } from './actions' 5 | import { TPluginOptions } from './interface' 6 | import { normalizeOptions } from './options' 7 | 8 | export const plugin = createPlugin({ 9 | debug: 'semantic-release:metabranch', 10 | async handler({ step, stepConfig, pluginConfig, context, debug }) { 11 | const stepOptions = stepConfig || pluginConfig[step] 12 | 13 | debug( 14 | 'handler exec:', 15 | 'step=', 16 | step, 17 | 'stepConfig=', 18 | JSON.stringify(stepOptions), 19 | ) 20 | 21 | if (!stepOptions) { 22 | return 23 | } 24 | 25 | const user = { 26 | name: context.env.GIT_AUTHOR_NAME || context.env.GIT_COMMITTER_NAME, 27 | email: context.env.GIT_AUTHOR_EMAIL || context.env.GIT_COMMITTER_EMAIL, 28 | } 29 | const { branch, from, to, message, action } = stepOptions as TPluginOptions 30 | const actionOptions = normalizeOptions({ 31 | branch, 32 | from, 33 | to, 34 | message, 35 | user, 36 | cwd: context.cwd, 37 | repo: context.options?.repositoryUrl + '', 38 | debug, 39 | }) 40 | actionOptions.message = tpl(actionOptions.message, context, context.logger) 41 | 42 | if (context.options?.dryRun && action === 'push') { 43 | context.logger.log( 44 | '[metabranch] `push` action is disabled in dry-run mode', 45 | ) 46 | 47 | return 48 | } 49 | 50 | await perform(action, actionOptions) 51 | }, 52 | }) 53 | -------------------------------------------------------------------------------- /packages/metabranch/src/test/fixtures/basicPackage/inner/file.txt: -------------------------------------------------------------------------------- 1 | contents 2 | -------------------------------------------------------------------------------- /packages/metabranch/src/test/fixtures/basicPackage/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msr-test-basic", 3 | "author": "Dave Houlbrooke =8.3" 9 | }, 10 | "release": { 11 | "plugins": [ 12 | "@semantic-release/commit-analyzer", 13 | "@semantic-release/release-notes-generator" 14 | ], 15 | "noCi": true 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/metabranch/src/test/fixtures/foo/bar/foobar.txt: -------------------------------------------------------------------------------- 1 | foobar 2 | -------------------------------------------------------------------------------- /packages/metabranch/src/test/fixtures/foo/baz/foobaz.txt: -------------------------------------------------------------------------------- 1 | foobaz 2 | -------------------------------------------------------------------------------- /packages/metabranch/src/test/fixtures/foo/foo/foofoo.txt: -------------------------------------------------------------------------------- 1 | foofoo 2 | -------------------------------------------------------------------------------- /packages/metabranch/src/test/ts/actions.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs' 2 | import path, { dirname} from 'node:path' 3 | import { fileURLToPath } from 'node:url' 4 | 5 | import { Debugger } from '@qiwi/semrel-plugin-creator' 6 | import { gitCreateFakeRepo } from '@qiwi/semrel-testing-suite' 7 | import { execa } from 'execa' 8 | import { temporaryDirectory } from 'tempy' 9 | 10 | import { perform, push, TActionOptionsNormalized } from '../../main/ts' 11 | 12 | const __filename = fileURLToPath(import.meta.url) 13 | const __dirname = dirname(__filename) 14 | const fixtures = path.resolve(__dirname, '../fixtures') 15 | 16 | describe('actions', () => { 17 | const user = { name: 'Foo Bar', email: 'foo@bar.com' } 18 | 19 | describe('perform()', () => { 20 | it('Throws an error on unsupported action', () => { 21 | // @ts-ignore 22 | return expect(perform('foo', {})).rejects.toThrowError( 23 | /unsupported action 'foo'/, 24 | ) 25 | }) 26 | }) 27 | 28 | describe('fetch()', () => { 29 | it('clones files from remote to target dir', async () => { 30 | const cwd = temporaryDirectory() 31 | const to = 'foo/bar/baz' 32 | const { url: repo } = gitCreateFakeRepo({ 33 | sync: true, 34 | commits: [ 35 | { 36 | message: 'feat: initial commit', 37 | from: `${fixtures}/basicPackage/`, 38 | }, 39 | ], 40 | }) 41 | const opts: TActionOptionsNormalized = { 42 | branch: 'master', 43 | from: '.', 44 | to, 45 | repo, 46 | cwd, 47 | temp: temporaryDirectory(), 48 | debug: console.log as Debugger, 49 | user, 50 | message: 'update meta', 51 | } 52 | 53 | await perform('fetch', opts) 54 | 55 | expect( 56 | fs 57 | .readFileSync(path.join(cwd, to, 'inner/file.txt'), { 58 | encoding: 'utf8', 59 | }) 60 | .trim(), 61 | ).toBe('contents') 62 | }) 63 | }) 64 | 65 | describe('push()', () => { 66 | it('pushes files to remote', async () => { 67 | const cwd = `${fixtures}/foo/` 68 | const { cwd: _cwd, url: repo } = gitCreateFakeRepo({ 69 | sync: true, 70 | commits: [ 71 | { 72 | message: 'feat: initial commit', 73 | from: `${fixtures}/basicPackage/`, 74 | }, 75 | ], 76 | }) 77 | const opts = { 78 | cwd, 79 | temp: temporaryDirectory(), 80 | branch: 'metabranch', 81 | from: ['bar', 'unknown'], 82 | to: 'baz', 83 | repo, 84 | debug: console.log as Debugger, 85 | message: 'update meta', 86 | user, 87 | } 88 | 89 | const commitId = await perform('push', opts) 90 | 91 | await execa('git', ['fetch', 'origin', 'metabranch'], { cwd: _cwd }) 92 | await execa('git', ['checkout', 'origin/metabranch'], { cwd: _cwd }) 93 | 94 | expect( 95 | (await execa('git', ['rev-parse', 'HEAD'], { cwd: _cwd })).stdout, 96 | ).toBe(commitId) 97 | expect( 98 | fs 99 | .readFileSync(path.join(_cwd, 'baz', 'foobar.txt'), { 100 | encoding: 'utf8', 101 | }) 102 | .trim(), 103 | ).toBe('foobar') 104 | }) 105 | 106 | it('handles racing issues', async () => { 107 | const cwd = `${fixtures}/foo/` 108 | const { url: repo, cwd: _cwd } = gitCreateFakeRepo({ 109 | sync: true, 110 | commits: [ 111 | { 112 | message: 'feat: initial commit', 113 | from: `${fixtures}/basicPackage/`, 114 | }, 115 | ], 116 | }) 117 | const debug = console.log as Debugger 118 | const message = 'update meta' 119 | const opts0 = { 120 | cwd, 121 | temp: temporaryDirectory(), 122 | branch: 'metabranch', 123 | from: 'bar', 124 | to: 'scope', 125 | repo, 126 | debug, 127 | user, 128 | message, 129 | } 130 | const opts1 = { 131 | cwd, 132 | temp: temporaryDirectory(), 133 | branch: 'metabranch', 134 | from: 'foo', 135 | to: 'scope', 136 | repo, 137 | debug, 138 | user, 139 | message, 140 | } 141 | const opts2 = { 142 | cwd, 143 | temp: temporaryDirectory(), 144 | branch: 'metabranch', 145 | from: 'baz', 146 | to: 'scope', 147 | repo, 148 | debug, 149 | user, 150 | message, 151 | } 152 | 153 | const pushedCommits = await Promise.all([ 154 | push(opts0), 155 | push(opts1), 156 | push(opts2), 157 | ]) 158 | 159 | await execa('git', ['fetch', 'origin', 'metabranch'], { cwd: _cwd }) 160 | await execa('git', ['checkout', 'origin/metabranch'], { cwd: _cwd }) 161 | const commits = ( 162 | await execa('git', ['log', '--pretty=%H', 'origin/metabranch'], { 163 | cwd: _cwd, 164 | }) 165 | ).stdout.split('\n') 166 | 167 | expect(pushedCommits.every((hash) => commits.includes(hash))).toBeTruthy() 168 | expect( 169 | fs 170 | .readFileSync(path.join(_cwd, 'scope', 'foofoo.txt'), { 171 | encoding: 'utf8', 172 | }) 173 | .trim(), 174 | ).toBe('foofoo') 175 | expect( 176 | fs 177 | .readFileSync(path.join(_cwd, 'scope', 'foobar.txt'), { 178 | encoding: 'utf8', 179 | }) 180 | .trim(), 181 | ).toBe('foobar') 182 | expect( 183 | fs 184 | .readFileSync(path.join(_cwd, 'scope', 'foobaz.txt'), { 185 | encoding: 'utf8', 186 | }) 187 | .trim(), 188 | ).toBe('foobaz') 189 | }) 190 | }) 191 | }) 192 | -------------------------------------------------------------------------------- /packages/metabranch/src/test/ts/index.ts: -------------------------------------------------------------------------------- 1 | import def, { plugin } from '../../main/ts' 2 | 3 | describe('index', () => { 4 | it('properly exports its inners', () => { 5 | expect(plugin).toBe(def) 6 | }) 7 | }) 8 | -------------------------------------------------------------------------------- /packages/metabranch/src/test/ts/plugin.ts: -------------------------------------------------------------------------------- 1 | import { dirname, resolve } from 'node:path' 2 | import {fileURLToPath} from 'node:url' 3 | 4 | import { jest } from '@jest/globals' 5 | import { cleanPath, gitCreateFakeRepo } from '@qiwi/semrel-testing-suite' 6 | import { createRequire } from 'module' 7 | import resolveFrom from 'resolve-from' 8 | import semanticRelease from 'semantic-release' 9 | 10 | const require = createRequire(import.meta.url) 11 | const __filename = fileURLToPath(import.meta.url) 12 | const __dirname = dirname(__filename) 13 | const fixtures = resolve(__dirname, '../fixtures') 14 | 15 | describe('plugin', () => { 16 | const pluginName = 'some-plugin' 17 | const { cwd } = gitCreateFakeRepo({ 18 | sync: true, 19 | commits: [ 20 | { 21 | message: 'feat: initial commit', 22 | from: `${fixtures}/basicPackage/`, 23 | }, 24 | ], 25 | }) 26 | const perform = jest.fn() 27 | 28 | beforeAll(async () => { 29 | const resolveFromSilent = require('resolve-from').silent 30 | 31 | jest.unstable_mockModule(require.resolve('../../main/ts/actions'), () => ({ perform, __esModule: true })) 32 | const mockedPlugin = (await import('../../main/ts/plugin')).plugin 33 | jest.mock(pluginName, () => mockedPlugin, { 34 | virtual: true, 35 | }) 36 | jest 37 | .spyOn(resolveFrom, 'silent') 38 | .mockImplementation((fromDir: string, moduleId: string) => { 39 | if (moduleId === pluginName) { 40 | return pluginName 41 | } 42 | 43 | return resolveFromSilent(fromDir, moduleId) as string 44 | }) 45 | }) 46 | 47 | afterAll(() => { 48 | jest.restoreAllMocks() 49 | jest.resetModules() 50 | }) 51 | 52 | afterEach(jest.clearAllMocks) 53 | 54 | const env = { 55 | ...process.env, 56 | TRAVIS_PULL_REQUEST_BRANCH: 'master', 57 | TRAVIS_BRANCH: 'master', 58 | GITHUB_REF: 'master', 59 | GITHUB_BASE_REF: 'master', 60 | } 61 | 62 | it('plugin is compatible with semrel', async () => { 63 | await semanticRelease( 64 | { 65 | branches: ['master'], 66 | dryRun: true, 67 | plugins: [ 68 | [ 69 | pluginName, 70 | { 71 | verifyConditions: { 72 | action: 'fetch', 73 | branch: 'metabranch', 74 | from: 'foo', 75 | to: 'bar', 76 | message: 'commit message', 77 | }, 78 | }, 79 | ], 80 | ], 81 | }, 82 | { 83 | cwd: cleanPath(cwd), 84 | env, 85 | }, 86 | ) 87 | 88 | expect(perform).toHaveBeenCalledWith('fetch', { 89 | branch: 'metabranch', 90 | from: 'foo', 91 | to: 'bar', 92 | cwd: expect.any(String), 93 | temp: expect.any(String), 94 | repo: expect.any(String), 95 | message: 'commit message', 96 | debug: expect.any(Function), 97 | user: { 98 | name: 'semantic-release-bot', 99 | email: 'semantic-release-bot@martynus.net', 100 | }, 101 | }) 102 | }, 15000) 103 | 104 | it('handles `dry-run` option', async () => { 105 | await semanticRelease( 106 | { 107 | branches: ['master'], 108 | dryRun: true, 109 | plugins: [ 110 | [ 111 | pluginName, 112 | { 113 | verifyConditions: { 114 | action: 'push', 115 | }, 116 | }, 117 | ], 118 | ], 119 | }, 120 | { 121 | cwd: cleanPath(cwd), 122 | env, 123 | }, 124 | ) 125 | }, 5000) 126 | 127 | expect(perform).not.toHaveBeenCalled() 128 | }) 129 | -------------------------------------------------------------------------------- /packages/metabranch/tsconfig.es5.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "target": "es5", 5 | "outDir": "target/es5", 6 | "module": "ES2015" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/metabranch/tsconfig.es6.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "target": "es6", 5 | "outDir": "target/es6", 6 | "module": "ES6" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/metabranch/tsconfig.esnext.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "target": "esnext", 5 | "module": "esnext", 6 | "outDir": "target/esnext" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/metabranch/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../packages/infra/tsconfig.compiler.json", 3 | "compilerOptions": { 4 | "rootDir": "./src/main/ts/", 5 | "baseUrl": "./src/main/ts/", 6 | "tsBuildInfoFile": "./buildcache/.tsbuildinfo" 7 | }, 8 | "references": [ 9 | { "path": "../testing-suite" }, 10 | { "path": "../plugin-creator" } 11 | ], 12 | "include": [ 13 | "src/main/**/*" 14 | ], 15 | "exclude": [ 16 | "node_modules" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /packages/metabranch/tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "target": "esnext", 5 | "module": "esnext", 6 | "importHelpers": false, 7 | "noEmitHelpers": false, 8 | "esModuleInterop": true 9 | }, 10 | "include": [ 11 | "src/**/*" 12 | ], 13 | "exclude": [ 14 | "node_modules" 15 | ] 16 | } -------------------------------------------------------------------------------- /packages/metabranch/typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@qiwi/semrel-metabranch", 3 | "out": "./docs", 4 | "exclude": ["src/test", "**/node_modules/**", "paralleljs"], 5 | "externalPattern": ["**/node_modules/**"], 6 | "excludePrivate": false, 7 | "hideGenerator": true, 8 | "readme": "README.md", 9 | "theme": "default", 10 | "tsconfig": "./tsconfig.es6.json" 11 | } 12 | -------------------------------------------------------------------------------- /packages/plugin-actions/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | 'eslint-config-qiwi', 4 | 'prettier', 5 | ], 6 | overrides: [ 7 | { 8 | files: ['./src/test/**/*.ts'], 9 | rules: { 10 | 'unicorn/consistent-function-scoping': 'off' 11 | } 12 | } 13 | ] 14 | }; 15 | -------------------------------------------------------------------------------- /packages/plugin-actions/README.md: -------------------------------------------------------------------------------- 1 | # @qiwi/semrel-actions 2 | [Semrel](https://github.com/semantic-release/semantic-release) plugin for data syncing with remote workspaces 3 | 4 | ## Motivation 5 | The main purpose of this plugin is to provide _non-blocking_ release flow (no commits, no conflicts), 6 | but keep the benefits of stateful operations like changelog appending, docs publishing and so on. 7 | 8 | ## Usage area 9 | * Shared build state 10 | * Cross-release semaphore 11 | * Build meta publishing: coverage, buildstamp 12 | 13 | ## Status 14 | 🚧 WIP 🚧 15 | 16 | ## Install 17 | ```shell script 18 | yarn add @qiwi/semrel-actions -D 19 | ``` 20 | 21 | ## Config examples 22 | ```json 23 | { 24 | "plugins": [ 25 | ["@qiwi/semrel-actions", { 26 | "providers": [ 27 | { 28 | "provider": "metabranch", 29 | "options": { 30 | "branch": "metabranch", 31 | "url": "repoUrl" 32 | } 33 | } 34 | ] 35 | }] 36 | ], 37 | "prepare": [ 38 | ["@qiwi/semrel-actions", { 39 | "actions": [{ 40 | "provider": "metabranch", 41 | "options": { 42 | "from": "--changelog.md", 43 | "to": "changelog.md" 44 | } 45 | }] 46 | }] 47 | ], 48 | "publish": [ 49 | ["@qiwi/semrel-actions", { 50 | "actions": [{ 51 | "provider": "metabranch", 52 | "options": { 53 | "from": ["docs/*", "coverage/*", "buildstamp.json"], 54 | "to": "/" 55 | } 56 | }, { 57 | "provider": "metabranch", 58 | "options": { 59 | "from": "changelog.md", 60 | "to": "--changelog.md" 61 | } 62 | }] 63 | }] 64 | ] 65 | } 66 | ``` 67 | -------------------------------------------------------------------------------- /packages/plugin-actions/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = {...require('../infra/jest.config.json')} 2 | -------------------------------------------------------------------------------- /packages/plugin-actions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@qiwi/semrel-actions", 3 | "version": "1.1.2", 4 | "publishConfig": { 5 | "access": "public" 6 | }, 7 | "description": "Semrel actions provider", 8 | "keywords": [], 9 | "main": "target/es5/index.js", 10 | "source": "target/ts/index.ts", 11 | "types": "target/es5/index.d.ts", 12 | "typescript": { 13 | "definition": "target/es5/index.d.ts" 14 | }, 15 | "files": [ 16 | "README.md", 17 | "CHANGELOG.md", 18 | "target", 19 | "typings", 20 | "flow-typed" 21 | ], 22 | "scripts": { 23 | "clean": "rimraf target typings flow-typed buildcache coverage docs", 24 | "lint": "eslint 'src/**/*.ts'", 25 | "lint:fix": "yarn lint --fix", 26 | "format": "prettier --write 'src/**/*.ts'", 27 | "test": "echo 'WIP' && exit 0 || concurrently yarn:lint yarn:test:unit yarn:test:depcheck", 28 | "test:unit": "jest --runInBand", 29 | "test:depcheck": "npm_config_yes=true npx -p depcheck -p @babel/parser@7.16.4 depcheck --ignores '@types/*,tslib,eslint-*,prettier-*,@qiwi/semrel-infra,@qiwi/semrel-common' --ignore-patterns 'typings,flow-typed/*'", 30 | "build": "concurrently yarn:build:es5 yarn:build:es6 yarn:build:ts yarn:build:libdef yarn:docs", 31 | "build:es5": "mkdirp target/es5 && tsc -p tsconfig.es5.json", 32 | "build:es6": "mkdirp target/es6 && tsc -p tsconfig.es6.json", 33 | "build:ts": "cpy ./ ../../../target/ts/ --dot --cwd=./src/main/ts/", 34 | "build:libdef": "libdefkit --tsconfig=tsconfig.es5.json --tsconfig=tsconfig.es6.json", 35 | "docs": "typedoc --options ./typedoc.json ./src/main/ts", 36 | "uglify": "for f in $(find target -name '*.js'); do short=${f%.js}; terser -c -m -o $short.js -- $f; done", 37 | "postupdate": "yarn && yarn build && yarn test" 38 | }, 39 | "dependencies": { 40 | "@qiwi/semrel-plugin-creator": "workspace:*", 41 | "tslib": "^2.4.0" 42 | }, 43 | "devDependencies": { 44 | "@qiwi/semrel-infra": "workspace:*" 45 | }, 46 | "repository": { 47 | "type": "git", 48 | "url": "git+https://github.com/qiwi/semantic-release-toolkit.git" 49 | }, 50 | "author": "Anton Golub ", 51 | "license": "MIT", 52 | "bugs": { 53 | "url": "https://github.com/qiwi/semantic-release-toolkit/issues" 54 | }, 55 | "homepage": "https://github.com/qiwi/semantic-release-toolkit/#readme", 56 | "prettier": "prettier-config-qiwi" 57 | } 58 | -------------------------------------------------------------------------------- /packages/plugin-actions/src/main/ts/index.ts: -------------------------------------------------------------------------------- 1 | import { createPlugin } from '@qiwi/semrel-plugin-creator' 2 | 3 | export * from './interface' 4 | 5 | export const plugin = createPlugin({ 6 | async handler({ step }) { 7 | console.log(step) 8 | }, 9 | exclude: ['analyzeCommits', 'generateNotes'], 10 | }) 11 | 12 | export default plugin 13 | -------------------------------------------------------------------------------- /packages/plugin-actions/src/main/ts/interface.ts: -------------------------------------------------------------------------------- 1 | export interface TAction { 2 | provider: string 3 | options: Record 4 | } 5 | 6 | export type TActions = Array 7 | 8 | export type TPluginConfig = { 9 | actions: TActions 10 | } 11 | -------------------------------------------------------------------------------- /packages/plugin-actions/src/main/ts/providers/metabranch.ts: -------------------------------------------------------------------------------- 1 | /* 2 | import { TAction } from '../interface' 3 | 4 | export type TSyncPoint = { 5 | direction: 'local' | 'remote' 6 | branch: string 7 | from: string | string[] 8 | to: string 9 | } 10 | 11 | export type TSyncBatch = { 12 | batch: Array 13 | } 14 | 15 | export interface TMetabranchPluginAction extends TAction { 16 | provider: 'metabranch' 17 | options: TSyncPoint | TSyncBatch 18 | } 19 | */ 20 | 21 | export const foo = 'bar' 22 | -------------------------------------------------------------------------------- /packages/plugin-actions/src/test/js/index.js: -------------------------------------------------------------------------------- 1 | // import def, { plugin } from '../../../target/es6' 2 | // 3 | // describe('export (es6)', () => { 4 | // it('`plugin` defined', () => { 5 | // expect(plugin).toEqual(expect.any(Object)) 6 | // }) 7 | // 8 | // it('`default` equals `plugin`', () => { 9 | // expect(def).toBe(plugin) 10 | // }) 11 | // }) 12 | 13 | describe('', () => { 14 | it('', () => {}) 15 | }) 16 | -------------------------------------------------------------------------------- /packages/plugin-actions/src/test/ts/index.ts: -------------------------------------------------------------------------------- 1 | import def, { plugin } from '../../main/ts' 2 | 3 | describe('export', () => { 4 | it('`plugin` defined', () => { 5 | expect(plugin).toEqual(expect.any(Object)) 6 | }) 7 | 8 | it('`default` equals `plugin`', () => { 9 | expect(def).toBe(plugin) 10 | }) 11 | }) 12 | -------------------------------------------------------------------------------- /packages/plugin-actions/tsconfig.es5.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "target": "es5", 5 | "outDir": "target/es5" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/plugin-actions/tsconfig.es6.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "target": "es6", 5 | "outDir": "target/es6" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/plugin-actions/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../packages/infra/tsconfig.compiler.json", 3 | "compilerOptions": { 4 | "rootDir": "./src/main/ts/", 5 | "baseUrl": "./src/main/ts/", 6 | "target": "es5", 7 | "outDir": "target/es5", 8 | "tsBuildInfoFile": "./buildcache/.tsbuildinfo" 9 | }, 10 | "references": [ 11 | { "path": "../plugin-creator" }, 12 | { "path": "../common" } 13 | ], 14 | "include": [ 15 | "src/main/**/*" 16 | ], 17 | "exclude": [ 18 | "node_modules" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /packages/plugin-actions/tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": [ 4 | "src/**/*" 5 | ], 6 | "exclude": [ 7 | "node_modules" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /packages/plugin-actions/typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@qiwi/semrel-actions", 3 | "out": "./docs", 4 | "exclude": ["src/test", "**/node_modules/**", "paralleljs"], 5 | "externalPattern": ["**/node_modules/**"], 6 | "excludePrivate": false, 7 | "hideGenerator": true, 8 | "readme": "README.md", 9 | "theme": "default", 10 | "tsconfig": "./tsconfig.es5.json" 11 | } 12 | -------------------------------------------------------------------------------- /packages/plugin-creator/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | 'eslint-config-qiwi', 4 | 'prettier', 5 | ], 6 | overrides: [ 7 | { 8 | files: ['./src/test/**/*.ts'], 9 | rules: { 10 | 'unicorn/consistent-function-scoping': 'off', 11 | 'sonarjs/no-duplicate-string': 'off' 12 | } 13 | } 14 | ] 15 | }; 16 | -------------------------------------------------------------------------------- /packages/plugin-creator/README.md: -------------------------------------------------------------------------------- 1 | # @qiwi/semrel-plugin-creator 2 | [Semrel](https://github.com/semantic-release/semantic-release) plugin creator 3 | 4 | ## Install 5 | ```shell script 6 | yarn add @qiwi/semrel-plugin-creator 7 | ``` 8 | 9 | ## Usage 10 | ```typescript 11 | import {createPlugin} from '@qiwi/semrel-plugin-creator' 12 | 13 | const handler = async ({step, pluginConfig, context, name}) => { 14 | if (step === 'prepare') { 15 | pluginConfig.foo = 'bar' 16 | } 17 | 18 | if (step === 'publish') { 19 | await doSomething() 20 | } 21 | } 22 | 23 | const plugin = createPlugin({ 24 | handler, 25 | name: 'plugin-name', 26 | include: ['prepare', 'publish'] 27 | }) 28 | ``` 29 | 30 | ## API 31 | ```typescript 32 | export type TPluginHandlerContext = { 33 | pluginConfig: TPluginConfig 34 | stepConfig: TPluginConfig 35 | stepConfigs: TStepConfigs 36 | context: TSemrelContext 37 | step: TReleaseStep 38 | } 39 | 40 | export type TPluginFactoryOptionsNormalized = { 41 | handler: TReleaseHandler 42 | name?: string 43 | include: TReleaseStep[] 44 | exclude: TReleaseStep[] 45 | require: TReleaseStep[] 46 | } 47 | 48 | export type TPluginFactoryOptions = Partial 49 | 50 | export type TReleaseHandler = (context: TPluginHandlerContext) => Promise 51 | 52 | export type TPluginFactory = ( 53 | handler: TPluginFactoryOptions | TReleaseHandler, 54 | ) => TPlugin 55 | ``` 56 | -------------------------------------------------------------------------------- /packages/plugin-creator/jest.config.mjs: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'node:fs' 2 | import { dirname, resolve } from 'node:path' 3 | import { fileURLToPath } from 'node:url' 4 | 5 | const _dirname = dirname(fileURLToPath(import.meta.url)) 6 | 7 | export default JSON.parse(readFileSync(resolve(_dirname, '../infra/jest.config.json'), { encoding: 'utf8' })) 8 | -------------------------------------------------------------------------------- /packages/plugin-creator/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@qiwi/semrel-plugin-creator", 3 | "version": "2.3.2", 4 | "publishConfig": { 5 | "access": "public" 6 | }, 7 | "description": "Semrel plugin creator", 8 | "exports": { 9 | ".": { 10 | "module": "./target/exports/es6.mjs", 11 | "import": "./target/exports/es6.mjs", 12 | "require": "./target/exports/es5.cjs" 13 | } 14 | }, 15 | "module": "./target/exports/es6.mjs", 16 | "source": "./target/ts/index.ts", 17 | "types": "./target/es6/index.d.ts", 18 | "keywords": [], 19 | "files": [ 20 | "README.md", 21 | "CHANGELOG.md", 22 | "target", 23 | "typings", 24 | "flow-typed" 25 | ], 26 | "scripts": { 27 | "clean": "rimraf target typings flow-typed buildcache coverage docs", 28 | "lint": "eslint 'src/**/*.ts'", 29 | "lint:fix": "yarn lint --fix", 30 | "format": "prettier --write 'src/**/*.ts'", 31 | "test": "concurrently yarn:lint yarn:test:unit yarn:test:depcheck", 32 | "test:unit": "NODE_OPTIONS=--experimental-vm-modules jest --config=jest.config.mjs --runInBand", 33 | "test:depcheck": "npm_config_yes=true npx -p depcheck -p @babel/parser@7.16.4 depcheck --ignores '@qiwi/esm,@jest/globals,@types/*,tslib,eslint-*,prettier-*,@qiwi/semrel-infra,@qiwi/semrel-common,@qiwi/semrel-plugin-creator' --ignore-patterns 'typings,flow-typed/*'", 34 | "build": "concurrently yarn:build:es5 yarn:build:es6 yarn:build:exports yarn:build:ts yarn:build:esnext yarn:build:libdef yarn:docs && yarn build:esmfix", 35 | "build:esnext": "mkdirp target/esnext && tsc -p tsconfig.esnext.json", 36 | "build:es5": "mkdirp target/es5 && tsc -p tsconfig.es5.json", 37 | "build:es6": "mkdirp target/es6 && tsc -p tsconfig.es6.json", 38 | "build:ts": "cpy ./ ../../../target/ts/ --dot --cwd=./src/main/ts/", 39 | "build:esmfix": "yarn tsc-esm-fix --target=target/es6 --target=target/esnext --dirnameVar=false --ext=.mjs", 40 | "build:exports": "cpy src/main/exports/ target/exports/ --flat", 41 | "build:libdef": "libdefkit --tsconfig=tsconfig.es5.json --tsconfig=tsconfig.es6.json --tsconfig=tsconfig.esnext.json", 42 | "docs": "typedoc --options ./typedoc.json ./src/main/ts", 43 | "uglify": "for f in $(find target -name '*.js'); do short=${f%.js}; terser -c -m -o $short.js -- $f; done", 44 | "postupdate": "yarn && yarn build && yarn test" 45 | }, 46 | "dependencies": { 47 | "@qiwi/esm": "^1.1.8", 48 | "@types/lodash-es": "^4.17.6", 49 | "@types/node": "^18.11.7", 50 | "@types/semantic-release": "^17.2.4", 51 | "debug": "^4.3.4", 52 | "lodash-es": "^4.17.21", 53 | "tslib": "^2.4.0" 54 | }, 55 | "devDependencies": { 56 | "@qiwi/semrel-infra": "workspace:*", 57 | "@qiwi/semrel-testing-suite": "workspace:*", 58 | "resolve-from": "^5.0.0", 59 | "semantic-release": "^19.0.5" 60 | }, 61 | "repository": { 62 | "type": "git", 63 | "url": "git+https://github.com/qiwi/semantic-release-toolkit.git" 64 | }, 65 | "author": "Anton Golub ", 66 | "license": "MIT", 67 | "bugs": { 68 | "url": "https://github.com/qiwi/semantic-release-toolkit/issues" 69 | }, 70 | "homepage": "https://github.com/qiwi/semantic-release-toolkit/#readme", 71 | "prettier": "prettier-config-qiwi", 72 | "main": "./target/exports/es5.cjs" 73 | } 74 | -------------------------------------------------------------------------------- /packages/plugin-creator/src/main/exports/es5.cjs: -------------------------------------------------------------------------------- 1 | require = require('@qiwi/esm')(module, { 2 | mode: 'all', 3 | cjs: { 4 | cache:false, 5 | esModule:true, 6 | extensions:true, 7 | namedExports:true 8 | }, 9 | force: false, 10 | cache: false, 11 | await: false, 12 | sourceMap: false, 13 | }) 14 | 15 | module.exports = require('../es5/index.js') 16 | -------------------------------------------------------------------------------- /packages/plugin-creator/src/main/exports/es6.mjs: -------------------------------------------------------------------------------- 1 | export * from '../es6/index.mjs' 2 | -------------------------------------------------------------------------------- /packages/plugin-creator/src/main/ts/index.ts: -------------------------------------------------------------------------------- 1 | import debugFactory, { Debugger } from 'debug' 2 | import { castArray } from 'lodash-es' 3 | 4 | import { 5 | TPlugin, 6 | TPluginConfig, 7 | TPluginFactory, 8 | TPluginFactoryOptions, 9 | TPluginFactoryOptionsNormalized, 10 | TPluginMetaContext, 11 | TReleaseHandler, 12 | TReleaseStep, 13 | TSemrelContext, 14 | } from './interface' 15 | 16 | export * from './interface' 17 | 18 | export const releaseSteps: Array = [ 19 | 'verifyConditions', 20 | 'analyzeCommits', 21 | 'verifyRelease', 22 | 'generateNotes', 23 | 'prepare', 24 | 'publish', 25 | 'addChannel', 26 | 'success', 27 | 'fail', 28 | ] 29 | 30 | export const defaultOptions = { 31 | include: releaseSteps, 32 | exclude: [], 33 | require: [], 34 | handler: async (): Promise => { 35 | /* async noop */ 36 | }, 37 | } 38 | 39 | const createDebugger = (scope: string | Debugger): Debugger => { 40 | if (typeof scope === 'string') { 41 | return debugFactory(scope) 42 | } 43 | 44 | return scope 45 | } 46 | 47 | export const normalizeOptions = ( 48 | options: TReleaseHandler | TPluginFactoryOptions, 49 | ): TPluginFactoryOptionsNormalized => { 50 | const preOptions = 51 | typeof options === 'function' ? { handler: options } : options 52 | const name = preOptions.name || `semrel-plugin-${Math.random().toString().slice(-5)}` 53 | const debug = createDebugger(preOptions.debug || name) 54 | 55 | return { ...defaultOptions, ...preOptions, debug, name } 56 | } 57 | 58 | const checkPrevSteps = ( 59 | { invoked }: TPluginMetaContext, 60 | { name, require }: TPluginFactoryOptionsNormalized, 61 | step: TReleaseStep, 62 | ): void => { 63 | if (require.length === 0) { 64 | return 65 | } 66 | 67 | const prevSteps = releaseSteps.slice(0, releaseSteps.indexOf(step)) 68 | const missedStep = prevSteps.find( 69 | (step) => require.includes(step) && !invoked.includes(step), 70 | ) 71 | 72 | if (missedStep) { 73 | throw new Error( 74 | `plugin '${name}' requires ${missedStep} to be invoked before ${step}`, 75 | ) 76 | } 77 | } 78 | 79 | export const getStepConfig = ( 80 | context: TSemrelContext, 81 | step: TReleaseStep, 82 | name = '', 83 | ): TPluginConfig | undefined => 84 | castArray(context.options?.[step]) 85 | .map((config) => { 86 | if (Array.isArray(config)) { 87 | const [path, opts] = config 88 | 89 | return { ...opts, path } 90 | } 91 | 92 | return config 93 | }) 94 | .find((config) => config?.path === name) 95 | 96 | export const getStepConfigs = ( 97 | context: TSemrelContext, 98 | name = '', 99 | ): Record => 100 | releaseSteps.reduce>( 101 | (configs, step) => { 102 | configs[step] = getStepConfig(context, step, name) 103 | 104 | return configs 105 | }, 106 | {} as Record, 107 | ) 108 | 109 | const metaContexts: WeakMap = new WeakMap() 110 | 111 | const getMetaContext = (context: TSemrelContext): TPluginMetaContext => { 112 | let metaContext = metaContexts.get(context) 113 | 114 | if (!metaContext) { 115 | metaContext = { 116 | invoked: [], 117 | } 118 | metaContexts.set(context, metaContext) 119 | } 120 | 121 | return metaContext 122 | } 123 | 124 | export const createPlugin: TPluginFactory = (options) => { 125 | const normalizedOpions = normalizeOptions(options) 126 | const { handler, include, exclude, name, debug } = normalizedOpions 127 | 128 | return releaseSteps 129 | .filter((step) => include.includes(step) && !exclude.includes(step)) 130 | .reduce((m, step) => { 131 | m[step] = (pluginConfig: TPluginConfig, context: TSemrelContext) => { 132 | const metaContext = getMetaContext(context) 133 | 134 | checkPrevSteps(metaContext, normalizedOpions, step) 135 | 136 | metaContext.invoked.push(step) 137 | 138 | const stepConfigs = getStepConfigs(context, name) 139 | const stepConfig = stepConfigs[step] 140 | 141 | return handler({ 142 | pluginConfig, 143 | context, 144 | step, 145 | stepConfig, 146 | stepConfigs, 147 | debug, 148 | }) 149 | } 150 | 151 | return m 152 | }, {}) 153 | } 154 | -------------------------------------------------------------------------------- /packages/plugin-creator/src/main/ts/interface.ts: -------------------------------------------------------------------------------- 1 | import { Debugger } from 'debug' 2 | import { Context } from 'semantic-release' 3 | 4 | export { Debugger } from 'debug' 5 | 6 | export type TReleaseType = 'patch' | 'minor' | 'major' 7 | 8 | export type TTag = { 9 | version: string 10 | channel: string 11 | gitTag: string 12 | gitHead: string 13 | } 14 | 15 | export type TBranch = { 16 | channel?: string 17 | tags: string[] 18 | type: string 19 | name: string 20 | range: string 21 | accept: TReleaseType[] 22 | main: boolean 23 | } 24 | 25 | export type TSemrelContext = Context & { 26 | cwd: string 27 | branch?: TBranch 28 | branches: string[] 29 | } 30 | 31 | export type TPluginConfig = Record 32 | 33 | export type TPluginMethod = ( 34 | pluginConfig: TPluginConfig, 35 | context: TSemrelContext, 36 | ) => Promise 37 | 38 | export interface TPlugin { 39 | verifyConditions?: TPluginMethod 40 | analyzeCommits?: TPluginMethod 41 | verifyRelease?: TPluginMethod 42 | generateNotes?: TPluginMethod 43 | prepare?: TPluginMethod 44 | publish?: TPluginMethod 45 | addChannel?: TPluginMethod 46 | success?: TPluginMethod 47 | fail?: TPluginMethod 48 | } 49 | 50 | export type TReleaseStep = keyof TPlugin 51 | 52 | export type TStepConfigs = Record 53 | 54 | export type TPluginHandlerContext = { 55 | pluginConfig: TPluginConfig 56 | stepConfig?: TPluginConfig 57 | stepConfigs: TStepConfigs 58 | context: TSemrelContext 59 | step: TReleaseStep 60 | debug: Debugger 61 | } 62 | 63 | export type TReleaseHandler = (context: TPluginHandlerContext) => Promise 64 | 65 | export type TPluginFactoryOptionsNormalized = { 66 | handler: TReleaseHandler 67 | name?: string 68 | include: TReleaseStep[] 69 | exclude: TReleaseStep[] 70 | require: TReleaseStep[] 71 | debug: Debugger 72 | } 73 | 74 | export type TPluginFactoryOptions = Partial< 75 | Omit & { 76 | debug: string | Debugger 77 | } 78 | > 79 | 80 | export type TPluginFactory = ( 81 | handler: TPluginFactoryOptions | TReleaseHandler, 82 | ) => TPlugin 83 | 84 | export type TPluginMetaContext = { 85 | invoked: TReleaseStep[] 86 | } 87 | -------------------------------------------------------------------------------- /packages/plugin-creator/src/test/cjs/index.cjs: -------------------------------------------------------------------------------- 1 | describe('awaits', () => { 2 | it('https://github.com/facebook/jest/pull/11961', () => { 3 | expect(true).toBe(true) 4 | }) 5 | }) 6 | 7 | // const {createPlugin} = require('@qiwi/semrel-plugin-creator') // eslint-disable-line 8 | // 9 | // describe('cjs', () => { 10 | // it('createPlugin', () => { 11 | // expect(createPlugin).toEqual(expect.any(Function)) 12 | // }) 13 | // }) 14 | -------------------------------------------------------------------------------- /packages/plugin-creator/src/test/fixtures/yarnWorkspaces/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msr-test-yarn", 3 | "author": "Dave Houlbrooke =8.3" 9 | }, 10 | "workspaces": [ 11 | "packages/*" 12 | ], 13 | "release": { 14 | "plugins": [ 15 | "@semantic-release/commit-analyzer", 16 | "@semantic-release/release-notes-generator" 17 | ], 18 | "noCi": true 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/plugin-creator/src/test/fixtures/yarnWorkspaces/packages/a/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msr-test-a", 3 | "version": "0.0.0", 4 | "peerDependencies": { 5 | "msr-test-c": "*", 6 | "left-pad": "latest" 7 | } 8 | } -------------------------------------------------------------------------------- /packages/plugin-creator/src/test/fixtures/yarnWorkspaces/packages/b/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msr-test-b", 3 | "version": "0.0.0", 4 | "dependencies": { 5 | "msr-test-a": "*" 6 | }, 7 | "devDependencies": { 8 | "msr-test-c": "*", 9 | "left-pad": "latest" 10 | } 11 | } -------------------------------------------------------------------------------- /packages/plugin-creator/src/test/fixtures/yarnWorkspaces/packages/c/.releaserc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tagFormat": "multi-semantic-release-test-c@v${version}" 3 | } -------------------------------------------------------------------------------- /packages/plugin-creator/src/test/fixtures/yarnWorkspaces/packages/c/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msr-test-c", 3 | "version": "0.0.0", 4 | "devDependencies": { 5 | "msr-test-b": "*", 6 | "msr-test-d": "*" 7 | } 8 | } -------------------------------------------------------------------------------- /packages/plugin-creator/src/test/fixtures/yarnWorkspaces/packages/d/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msr-test-d", 3 | "version": "0.0.0" 4 | } 5 | -------------------------------------------------------------------------------- /packages/plugin-creator/src/test/mjs/index.mjs: -------------------------------------------------------------------------------- 1 | import {createPlugin} from '@qiwi/semrel-plugin-creator' 2 | 3 | describe('esm/mjs', () => { 4 | it('createPlugin', () => { 5 | expect(createPlugin).toEqual(expect.any(Function)) 6 | }) 7 | }) 8 | -------------------------------------------------------------------------------- /packages/plugin-creator/src/test/ts/index.ts: -------------------------------------------------------------------------------- 1 | import { jest } from '@jest/globals' 2 | 3 | import { 4 | createPlugin, 5 | defaultOptions, 6 | normalizeOptions, 7 | releaseSteps, 8 | TBranch, 9 | TPluginFactoryOptions, 10 | TPluginMethod, 11 | TSemrelContext, 12 | } from '../../main/ts' 13 | 14 | describe('normalizeOptions()', () => { 15 | it('handles fn as input', () => { 16 | const handler: any = jest.fn() 17 | expect(normalizeOptions(handler).handler).toBe(handler) 18 | }) 19 | 20 | it('merges `defaultOptions` with passed opts', () => { 21 | const handler: any = jest.fn() 22 | const name = 'fixtures.foobar' 23 | const options: TPluginFactoryOptions = { 24 | handler, 25 | include: ['fail'], 26 | exclude: ['success'], 27 | require: [], 28 | name, 29 | debug: 'scope', 30 | } 31 | 32 | expect(normalizeOptions(options)).toMatchObject({ 33 | ...options, 34 | debug: expect.any(Function), 35 | }) 36 | }) 37 | 38 | it('uses `defaultOptions` otherwise', () => { 39 | expect(normalizeOptions({})).toMatchObject(defaultOptions) 40 | }) 41 | 42 | it('resolves plugin`s pkg name', () => { 43 | expect(normalizeOptions({ name: 'foo' }).name).toBe('foo') 44 | expect(normalizeOptions({}).name).toMatch('semrel-plugin-') 45 | }) 46 | }) 47 | 48 | describe('createPlugin()', () => { 49 | it('factory returns a new plugin each time', () => { 50 | const handler: any = jest.fn() 51 | const plugin1 = createPlugin(handler) 52 | const plugin2 = createPlugin(handler) 53 | 54 | expect(plugin1).toBeInstanceOf(Object) 55 | expect(plugin2).toBeInstanceOf(Object) 56 | expect(plugin1).not.toBe(plugin2) 57 | }) 58 | 59 | it('plugin properly invokes inner handler', () => { 60 | const handler: any = jest.fn() 61 | const pluginName = 'some-plugin' 62 | const plugin = createPlugin({ handler, name: pluginName }) 63 | const pluginConfig = {} 64 | const branches = ['master'] 65 | const logger: any = console 66 | const branch: TBranch = { 67 | tags: [], 68 | type: 'release', 69 | name: 'master', 70 | range: '>1.0.0', 71 | accept: ['patch', 'minor', 'major'], 72 | main: true, 73 | } 74 | const context: TSemrelContext = { 75 | cwd: process.cwd(), 76 | branch, 77 | branches, 78 | logger, 79 | env: {}, 80 | options: { 81 | branch, 82 | branches, 83 | repositoryUrl: 'url', 84 | tagFormat: '@${version}', // eslint-disable-line no-template-curly-in-string 85 | plugins: [['some-plugin', { a: 'b' }]], 86 | publish: [ 87 | { 88 | path: 'some-plugin', 89 | foo: 'bar', 90 | }, 91 | { 92 | path: 'other-plugin', 93 | bar: 'baz', 94 | }, 95 | ], 96 | }, 97 | } 98 | 99 | releaseSteps.forEach((step) => { 100 | ;(plugin[step] as TPluginMethod)(pluginConfig, context) 101 | 102 | expect(handler).toHaveBeenCalledWith({ 103 | pluginConfig, 104 | context, 105 | step, 106 | stepConfig: 107 | step === 'publish' 108 | ? { 109 | path: 'some-plugin', 110 | foo: 'bar', 111 | } 112 | : undefined, 113 | stepConfigs: { 114 | verifyConditions: undefined, 115 | analyzeCommits: undefined, 116 | verifyRelease: undefined, 117 | generateNotes: undefined, 118 | prepare: undefined, 119 | publish: { 120 | path: 'some-plugin', 121 | foo: 'bar', 122 | }, 123 | addChannel: undefined, 124 | success: undefined, 125 | fail: undefined, 126 | }, 127 | debug: expect.any(Function), 128 | }) 129 | }) 130 | 131 | expect(handler).toBeCalledTimes(releaseSteps.length) 132 | }) 133 | 134 | describe('options', () => { 135 | it('plugin exposes `included` methods only if option passed', () => { 136 | const plugin = createPlugin({ include: ['success', 'prepare'] }) 137 | 138 | expect(Object.keys(plugin)).toEqual(['prepare', 'success']) 139 | }) 140 | 141 | it('plugin omits `excluded` methods if option passed', () => { 142 | const plugin = createPlugin({ exclude: ['success', 'prepare'] }) 143 | 144 | expect(Object.keys(plugin)).toEqual( 145 | releaseSteps.filter((step) => step !== 'success' && step !== 'prepare'), 146 | ) 147 | }) 148 | 149 | it('`require` option asserts that all plugin steps have been called', () => { 150 | const plugin = createPlugin({ require: ['verifyConditions', 'prepare'] }) 151 | const pluginConfig = {} 152 | const logger: any = console 153 | const context: TSemrelContext = { 154 | cwd: process.cwd(), 155 | branch: { 156 | tags: [], 157 | type: 'release', 158 | name: 'master', 159 | range: '>1.0.0', 160 | accept: ['patch', 'minor', 'major'], 161 | main: true, 162 | }, 163 | branches: ['master'], 164 | logger, 165 | env: {}, 166 | } 167 | const verifyConditions = plugin.verifyConditions as TPluginMethod 168 | const analyzeCommits = plugin.analyzeCommits as TPluginMethod 169 | const prepare = plugin.prepare as TPluginMethod 170 | const publish = plugin.publish as TPluginMethod 171 | 172 | expect(() => analyzeCommits(pluginConfig, context)).toThrowError( 173 | /^plugin 'semrel-plugin-\d{5}' requires verifyConditions to be invoked before analyzeCommits$/, 174 | ) 175 | 176 | verifyConditions(pluginConfig, context) 177 | analyzeCommits(pluginConfig, context) 178 | 179 | expect(() => publish(pluginConfig, context)).toThrowError( 180 | /^plugin 'semrel-plugin-\d{5}' requires prepare to be invoked before publish$/, 181 | ) 182 | 183 | prepare(pluginConfig, context) 184 | publish(pluginConfig, context) 185 | }) 186 | }) 187 | 188 | describe('metaContext', () => { 189 | it('is unique for each semrel context', () => { 190 | const plugin = createPlugin({ require: ['verifyConditions'] }) 191 | const pluginConfig = {} 192 | const verifyConditions = plugin.verifyConditions as TPluginMethod 193 | const analyzeCommits = plugin.analyzeCommits as TPluginMethod 194 | const logger: any = console 195 | const context1: TSemrelContext = { 196 | cwd: process.cwd(), 197 | branch: { 198 | tags: [], 199 | type: 'release', 200 | name: 'master', 201 | range: '>1.0.0', 202 | accept: ['patch', 'minor', 'major'], 203 | main: true, 204 | }, 205 | branches: ['master'], 206 | logger, 207 | env: {}, 208 | } 209 | const context2: TSemrelContext = { 210 | cwd: process.cwd(), 211 | branch: { 212 | tags: [], 213 | type: 'release', 214 | name: 'master', 215 | range: '>1.0.0', 216 | accept: ['patch', 'minor', 'major'], 217 | main: true, 218 | }, 219 | branches: ['master'], 220 | logger, 221 | env: {}, 222 | } 223 | 224 | expect(() => analyzeCommits(pluginConfig, context1)).toThrowError( 225 | /^plugin 'semrel-plugin-\d{5}' requires verifyConditions to be invoked before analyzeCommits$/, 226 | ) 227 | verifyConditions(pluginConfig, context1) 228 | analyzeCommits(pluginConfig, context1) 229 | 230 | expect(() => analyzeCommits(pluginConfig, context2)).toThrowError( 231 | /^plugin 'semrel-plugin-\d{5}' requires verifyConditions to be invoked before analyzeCommits$/, 232 | ) 233 | verifyConditions(pluginConfig, context2) 234 | analyzeCommits(pluginConfig, context2) 235 | }) 236 | }) 237 | }) 238 | -------------------------------------------------------------------------------- /packages/plugin-creator/src/test/ts/integration.ts: -------------------------------------------------------------------------------- 1 | import { dirname, resolve } from 'node:path' 2 | import { fileURLToPath } from 'node:url' 3 | 4 | import { jest } from '@jest/globals' 5 | import { 6 | cleanPath, 7 | gitCreateFakeRepo, 8 | } from '@qiwi/semrel-testing-suite' 9 | import resolveFrom, { silent as resolveFromSilent } from 'resolve-from' 10 | import semanticRelease from 'semantic-release' 11 | 12 | import { createPlugin } from '../../main/ts' 13 | 14 | const __filename = fileURLToPath(import.meta.url) 15 | const __dirname = dirname(__filename) 16 | const fixtures = resolve(__dirname, '../fixtures') 17 | 18 | describe('integration', () => { 19 | const handler: any = jest.fn(({ step }) => { 20 | if (step === 'analyzeCommits') { 21 | return 'patch' 22 | } 23 | }) 24 | const pluginName = 'some-plugin' 25 | const plugin = createPlugin({ handler, name: pluginName }) 26 | const { cwd } = gitCreateFakeRepo({ 27 | sync: true, 28 | commits: [ 29 | { 30 | message: 'feat: initial commit', 31 | from: `${fixtures}/yarnWorkspaces/`, 32 | }, 33 | ], 34 | }) 35 | 36 | beforeAll(() => { 37 | jest.mock(pluginName, () => plugin, { virtual: true }) 38 | jest 39 | .spyOn(resolveFrom, 'silent') 40 | .mockImplementation((fromDir: string, moduleId: string) => { 41 | if (moduleId === pluginName) { 42 | return pluginName 43 | } 44 | 45 | return resolveFromSilent(fromDir, moduleId) as string 46 | }) 47 | }) 48 | 49 | afterAll(() => { 50 | jest.restoreAllMocks() 51 | jest.resetModules() 52 | }) 53 | 54 | afterEach(jest.clearAllMocks) 55 | 56 | const env = { 57 | ...process.env, 58 | TRAVIS_PULL_REQUEST_BRANCH: 'master', 59 | TRAVIS_BRANCH: 'master', 60 | GITHUB_REF: 'master', 61 | GITHUB_BASE_REF: 'master', 62 | } 63 | 64 | it('plugin is compatible with semrel', async () => { 65 | await semanticRelease( 66 | { 67 | branches: ['master'], 68 | dryRun: true, 69 | plugins: [pluginName], 70 | }, 71 | { 72 | cwd: cleanPath(cwd), 73 | env, 74 | }, 75 | ) 76 | 77 | expect(handler).toBeCalledTimes(4) 78 | }, 15000) 79 | 80 | it('release handler is invoked with proper context', async () => { 81 | const commonPluginConfig = { common: true } 82 | const preparePluginConfig = { prepare: true } 83 | const publishPluginConfig = { publish: true } 84 | const stepConfigs = { 85 | prepare: preparePluginConfig, 86 | publish: publishPluginConfig, 87 | } 88 | 89 | await semanticRelease( 90 | { 91 | branches: ['master'], 92 | dryRun: false, 93 | plugins: [[pluginName, commonPluginConfig]], 94 | prepare: [[pluginName, preparePluginConfig]], 95 | publish: [ 96 | { 97 | path: pluginName, 98 | ...publishPluginConfig, 99 | }, 100 | ], 101 | }, 102 | { 103 | cwd: cleanPath(cwd), 104 | env, 105 | }, 106 | ) 107 | 108 | const expectedContext = { 109 | env, 110 | } 111 | // prettier-ignore 112 | const expectedArgs = [ 113 | {step: 'verifyConditions', pluginConfig: commonPluginConfig, context: expectedContext}, 114 | {step: 'analyzeCommits', context: expectedContext}, 115 | {step: 'verifyRelease', context: expectedContext}, 116 | {step: 'generateNotes'}, 117 | { 118 | step: 'prepare', 119 | pluginConfig: preparePluginConfig, 120 | stepConfig: preparePluginConfig, 121 | stepConfigs, 122 | context: { 123 | ...expectedContext, 124 | nextRelease: { 125 | type: 'patch', 126 | version: '1.0.0', 127 | gitTag: 'v1.0.0', 128 | name: 'v1.0.0', 129 | notes: '' 130 | } 131 | } 132 | }, 133 | {step: 'publish', pluginConfig: publishPluginConfig, stepConfig: publishPluginConfig, stepConfigs}, 134 | {step: 'success'}, 135 | ] 136 | expectedArgs.forEach((handlerContext, index) => 137 | expect(handler.mock.calls[index][0]).toMatchObject(handlerContext), 138 | ) 139 | 140 | expect(handler).toBeCalledTimes(7) 141 | }, 15000) 142 | }) 143 | -------------------------------------------------------------------------------- /packages/plugin-creator/tsconfig.es5.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "target": "es5", 5 | "outDir": "target/es5", 6 | "module": "ES2015" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/plugin-creator/tsconfig.es6.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "target": "es6", 5 | "outDir": "target/es6", 6 | "module": "ES6" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/plugin-creator/tsconfig.esnext.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "target": "esnext", 5 | "module": "esnext", 6 | "outDir": "target/esnext" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/plugin-creator/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../packages/infra/tsconfig.compiler.json", 3 | "compilerOptions": { 4 | "rootDir": "./src/main/ts/", 5 | "baseUrl": "./src/main/ts/", 6 | "tsBuildInfoFile": "./buildcache/.tsbuildinfo" 7 | }, 8 | "references": [ 9 | { "path": "../testing-suite" } 10 | ], 11 | "include": [ 12 | "src/main/**/*" 13 | ], 14 | "exclude": [ 15 | "node_modules" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /packages/plugin-creator/tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "target": "esnext", 5 | "module": "esnext", 6 | "importHelpers": false, 7 | "noEmitHelpers": false, 8 | "esModuleInterop": true 9 | }, 10 | "include": [ 11 | "src/**/*" 12 | ], 13 | "exclude": [ 14 | "node_modules" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /packages/plugin-creator/typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@qiwi/semrel-plugin-creator", 3 | "out": "./docs", 4 | "exclude": ["src/test", "**/node_modules/**", "paralleljs"], 5 | "externalPattern": ["**/node_modules/**"], 6 | "excludePrivate": false, 7 | "hideGenerator": true, 8 | "readme": "README.md", 9 | "theme": "default", 10 | "tsconfig": "./tsconfig.es5.json" 11 | } 12 | -------------------------------------------------------------------------------- /packages/preset/README.md: -------------------------------------------------------------------------------- 1 | # @qiwi/semrel-preset 2 | Shared QIWI OSS plugin preset for [semantic-release](https://github.com/semantic-release/semantic-release) 3 | What's inside: 4 | * [@qiwi/semantic-release-gh-pages-plugin](https://github.com/qiwi/semantic-release-gh-pages-plugin) 5 | * [@semantic-release/commit-analyzer](https://github.com/semantic-release/commit-analyzer) 6 | * [@semantic-release/release-notes-generator](https://github.com/semantic-release/release-notes-generator) 7 | * [@semantic-release/changelog](https://github.com/semantic-release/changelog) 8 | * [@semantic-release/npm](https://github.com/semantic-release/npm) 9 | * [@semantic-release/github](https://github.com/semantic-release/github) 10 | * [@semantic-release/git](https://github.com/semantic-release/git) 11 | * [@semantic-release/exec](https://github.com/semantic-release/exec) 12 | 13 | ## Usage 14 | ```shell script 15 | yarn add @qiwi/semrel-preset -D 16 | ``` 17 | -------------------------------------------------------------------------------- /packages/preset/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = {...require('../infra/jest.config.json')} 2 | -------------------------------------------------------------------------------- /packages/preset/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@qiwi/semrel-preset", 3 | "version": "3.1.10", 4 | "publishConfig": { 5 | "access": "public" 6 | }, 7 | "description": "QIWI OSS semrel plugins preset", 8 | "keywords": [], 9 | "files": [ 10 | "README.md", 11 | "CHANGELOG.md" 12 | ], 13 | "scripts": { 14 | "test": "yarn test:unit", 15 | "test:unit": "jest --runInBand", 16 | "clean": "rimraf target", 17 | "postupdate": "yarn && yarn build && yarn test", 18 | "format": "prettier --write 'src/**/*.ts'" 19 | }, 20 | "dependencies": { 21 | "@qiwi/semantic-release-gh-pages-plugin": "^5.2.3", 22 | "@qiwi/semrel-metabranch": "workspace:*", 23 | "@semantic-release/changelog": "^6.0.1", 24 | "@semantic-release/commit-analyzer": "^9.0.2", 25 | "@semantic-release/exec": "^6.0.3", 26 | "@semantic-release/git": "^10.0.1", 27 | "@semantic-release/github": "^8.0.6", 28 | "@semantic-release/npm": "^9.0.1", 29 | "@semantic-release/release-notes-generator": "^10.0.3", 30 | "@semrel-extra/npm": "^1.2.2" 31 | }, 32 | "peerDependencies": { 33 | "semantic-release": "*" 34 | }, 35 | "repository": { 36 | "type": "git", 37 | "url": "git+https://github.com/qiwi/semantic-release-toolkit.git" 38 | }, 39 | "author": "Anton Golub ", 40 | "license": "MIT", 41 | "bugs": { 42 | "url": "https://github.com/qiwi/semantic-release-toolkit/issues" 43 | }, 44 | "homepage": "https://github.com/qiwi/semantic-release-toolkit/#readme" 45 | } 46 | -------------------------------------------------------------------------------- /packages/preset/src/test/js/index.js: -------------------------------------------------------------------------------- 1 | describe('@qiwi/semrel-preset', () => { 2 | const plugins = [ 3 | '@qiwi/semantic-release-gh-pages-plugin', 4 | '@semantic-release/commit-analyzer', 5 | '@semantic-release/release-notes-generator', 6 | '@semantic-release/changelog', 7 | '@semantic-release/npm', 8 | '@semantic-release/github', 9 | '@semantic-release/git' 10 | ] 11 | 12 | plugins.forEach((plugin) => { 13 | it(`contains ${plugin}`, () => { 14 | expect(require(plugin)).not.toBeUndefined() 15 | }) 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /packages/testing-suite/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | 'eslint-config-qiwi', 4 | 'prettier', 5 | ], 6 | overrides: [ 7 | { 8 | files: ['./src/test/**/*.ts'], 9 | rules: { 10 | 'unicorn/consistent-function-scoping': 'off', 11 | 'sonarjs/no-duplicate-string': 'off' 12 | } 13 | }, 14 | { 15 | files: ['./src/main/ts/git.ts'], 16 | rules: { 17 | 'sonarjs/no-duplicate-string': 'off' 18 | } 19 | } 20 | ] 21 | }; 22 | -------------------------------------------------------------------------------- /packages/testing-suite/README.md: -------------------------------------------------------------------------------- 1 | # @qiwi/semrel-testing-suite 2 | Semrel/msr testing helpers 3 | 4 | ## Install 5 | ```shell script 6 | yarn add @qiwi/semrel-testing-suite -D 7 | ``` 8 | 9 | ## Usage 10 | ```ts 11 | import { resolve } from 'path' 12 | import resolveFrom, { silent as resolveFromSilent } from 'resolve-from' 13 | import semanticRelease from 'semantic-release' 14 | import { 15 | cleanPath, 16 | copyDirectory, 17 | gitCommitAll, 18 | gitInit, 19 | gitInitOrigin, 20 | gitPush 21 | } from '@qiwi/semrel-testing-suite' 22 | import { createPlugin } from '@qiwi/semrel-plugin-creator' 23 | 24 | const fixtures = resolve(__dirname, '../fixtures') 25 | 26 | describe('integration', () => { 27 | const handler: any = jest.fn(({step}) => { 28 | if (step === 'analyzeCommits') { 29 | return 'patch' 30 | } 31 | }) 32 | const pluginName = 'some-plugin' 33 | const plugin = createPlugin({handler, name: pluginName}) 34 | const cwd = gitInit() 35 | 36 | copyDirectory(`${fixtures}/yarnWorkspaces/`, cwd) 37 | gitCommitAll(cwd, 'feat: Initial release') 38 | gitInitOrigin(cwd) 39 | gitPush(cwd) 40 | 41 | beforeAll(() => { 42 | jest.mock(pluginName, () => plugin, {virtual: true}) 43 | jest 44 | .spyOn(resolveFrom, 'silent') 45 | .mockImplementation((fromDir: string, moduleId: string) => { 46 | if (moduleId === pluginName) { 47 | return pluginName 48 | } 49 | 50 | return resolveFromSilent(fromDir, moduleId) as string 51 | }) 52 | }) 53 | 54 | afterAll(() => { 55 | jest.restoreAllMocks() 56 | jest.resetModules() 57 | }) 58 | 59 | afterEach(jest.clearAllMocks) 60 | 61 | const env = { 62 | ...process.env, 63 | TRAVIS_PULL_REQUEST_BRANCH: 'master', 64 | TRAVIS_BRANCH: 'master' 65 | } 66 | 67 | it('plugin is compatible with semrel', async () => { 68 | await semanticRelease( 69 | { 70 | branches: ['master'], 71 | dryRun: true, 72 | plugins: [pluginName], 73 | }, 74 | { 75 | cwd: cleanPath(cwd), 76 | env, 77 | }, 78 | ) 79 | 80 | expect(handler).toBeCalledTimes(4) 81 | }, 15000) 82 | }) 83 | ``` 84 | -------------------------------------------------------------------------------- /packages/testing-suite/jest.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = {...require('../infra/jest.config.json')} 2 | -------------------------------------------------------------------------------- /packages/testing-suite/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@qiwi/semrel-testing-suite", 3 | "version": "3.1.2", 4 | "publishConfig": { 5 | "access": "public" 6 | }, 7 | "description": "Semrel/msr testing helpers", 8 | "keywords": [], 9 | "type": "module", 10 | "module": "./target/es6/index.mjs", 11 | "exports": "./target/es6/index.mjs", 12 | "source": "./target/ts/index.ts", 13 | "types": "./target/es6/index.d.ts", 14 | "typescript": { 15 | "definition": "./typings/index.d.ts" 16 | }, 17 | "files": [ 18 | "README.md", 19 | "CHANGELOG.md", 20 | "target", 21 | "typings", 22 | "flow-typed" 23 | ], 24 | "scripts": { 25 | "clean": "rimraf target typings flow-typed buildcache coverage docs", 26 | "lint": "eslint 'src/**/*.ts'", 27 | "lint:fix": "yarn lint --fix", 28 | "format": "prettier --write 'src/**/*.ts'", 29 | "test": "concurrently yarn:lint yarn:test:unit yarn:test:depcheck", 30 | "test:unit": "NODE_OPTIONS=--experimental-vm-modules jest --detectOpenHandles --forceExit --runInBand", 31 | "test:depcheck": "npm_config_yes=true npx -p depcheck -p @babel/parser@7.16.4 depcheck --ignores '@types/*,tslib,eslint-*,prettier-*,@qiwi/semrel-infra,@qiwi/semrel-common' --ignore-patterns 'typings,flow-typed/*'", 32 | "build": "concurrently yarn:build:esnext yarn:build:es6 yarn:build:ts yarn:build:libdef yarn:docs && yarn build:esmfix", 33 | "build:esnext": "mkdirp target/esnext && tsc -p tsconfig.esnext.json", 34 | "build:es6": "mkdirp target/es6 && tsc -p tsconfig.es6.json", 35 | "build:ts": "cpy ./ ../../../target/ts/ --dot --cwd=./src/main/ts/ --flat", 36 | "build:libdef": "libdefkit --tsconfig=tsconfig.esnext.json --tsconfig=tsconfig.es6.json --entry=@qiwi/semrel-testing-suite/target/es6", 37 | "build:esmfix": "yarn tsc-esm-fix --target=target/es6 --target=target/esnext --ext=.mjs", 38 | "docs": "typedoc --options ./typedoc.json ./src/main/ts", 39 | "uglify": "for f in $(find target -name '*.js'); do short=${f%.js}; terser -c -m -o $short.js -- $f; done", 40 | "postupdate": "yarn && yarn build && yarn test" 41 | }, 42 | "dependencies": { 43 | "@antongolub/git-root": "^1.5.7", 44 | "@qiwi/git-utils": "workspace:*", 45 | "@qiwi/semrel-common": "workspace:*", 46 | "@qiwi/substrate": "^1.20.15", 47 | "execa": "^6.1.0", 48 | "fs-extra": "^10.1.0", 49 | "tempy": "^3.0.0", 50 | "tslib": "^2.4.0" 51 | }, 52 | "devDependencies": { 53 | "@qiwi/semrel-infra": "workspace:*", 54 | "@types/node": "^18.11.7" 55 | }, 56 | "repository": { 57 | "type": "git", 58 | "url": "git+https://github.com/qiwi/semantic-release-toolkit.git" 59 | }, 60 | "author": "Anton Golub ", 61 | "license": "MIT", 62 | "bugs": { 63 | "url": "https://github.com/qiwi/semantic-release-toolkit/issues" 64 | }, 65 | "homepage": "https://github.com/qiwi/semantic-release-toolkit/#readme", 66 | "prettier": "prettier-config-qiwi" 67 | } 68 | -------------------------------------------------------------------------------- /packages/testing-suite/src/main/ts/file.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, lstatSync, writeFileSync } from 'node:fs' 2 | import { isAbsolute, join, normalize } from 'node:path' 3 | 4 | import fse from 'fs-extra' 5 | 6 | const { ensureFileSync, copySync: copyDirectory } = fse 7 | 8 | export { copyDirectory } 9 | 10 | // Is given path a directory? 11 | export const isDirectory = (path: string): boolean => { 12 | // String path that exists and is a directory. 13 | return ( 14 | typeof path === 'string' && 15 | existsSync(path) && 16 | lstatSync(path).isDirectory() 17 | ) 18 | } 19 | 20 | // Creates testing files on all specified folders. 21 | export const createTestingFiles = (cwd: string, folders: string[]): void => 22 | folders.forEach((fld) => { 23 | const target = join(cwd, fld, 'test.txt') 24 | 25 | ensureFileSync(target) 26 | writeFileSync(target, fld) 27 | }) 28 | 29 | /** 30 | * Normalize and make a path absolute, optionally using a custom CWD. 31 | * Trims any trailing slashes from the path. 32 | * 33 | * @param {string} path The path to normalize and make absolute. 34 | * @param {string} cwd=process.cwd() The CWD to prepend to the path to make it absolute. 35 | * @returns {string} The absolute and normalized path. 36 | * 37 | * @internal 38 | */ 39 | export const cleanPath = (path: string, cwd = process.cwd()): string => { 40 | // Checks. 41 | // check(path, "path: path"); 42 | // check(cwd, "cwd: absolute"); 43 | 44 | // Normalize, absolutify, and trim trailing slashes from the path. 45 | return normalize(isAbsolute(path) ? path : join(cwd, path)).replace( 46 | /[/\\]+$/, 47 | '', 48 | ) 49 | } 50 | -------------------------------------------------------------------------------- /packages/testing-suite/src/main/ts/git.ts: -------------------------------------------------------------------------------- 1 | import { gitRoot } from '@antongolub/git-root' 2 | import { 3 | exec, 4 | format, 5 | gitAddAll, 6 | gitCheckout, 7 | gitCommit, 8 | gitConfigAdd, 9 | gitExec, 10 | gitInit, 11 | gitInitOrigin, 12 | gitInitRemote, 13 | gitPush, 14 | gitRemoteAdd, 15 | gitSetUser, 16 | gitTag, 17 | IGitCommit, 18 | IGitInit, 19 | TGitResult, 20 | } from '@qiwi/git-utils' 21 | import { ICallable } from '@qiwi/substrate' 22 | import { temporaryDirectory } from 'tempy' 23 | 24 | import { copyDirectory } from './file' 25 | 26 | export * from '@qiwi/git-utils' 27 | 28 | export type TGitCommitDigest = { 29 | message: string 30 | from?: string 31 | tag?: string 32 | branch?: string 33 | } 34 | 35 | export interface IGitInitTestingRepo extends IGitInit { 36 | branch?: string 37 | commits?: TGitCommitDigest[] 38 | } 39 | 40 | /** 41 | * Create a Git repository. 42 | * _Created in a temp folder._ 43 | * 44 | * @param {string} opts.branch='master' The branch to initialize the repository to. 45 | * @return {Promise} Promise that resolves to string pointing to the CWD for the created Git repository. 46 | */ 47 | export const gitInitTestingRepo = ({ 48 | branch = 'master', 49 | sync, 50 | commits, 51 | }: T): TGitResult => { 52 | const cwd = temporaryDirectory() 53 | 54 | return exec( 55 | () => gitInit({ sync, cwd }), 56 | () => gitSetUser({ sync, cwd, name: 'Foo Bar', email: 'foo@bar.com' }), 57 | () => 58 | gitCheckout({ 59 | cwd, 60 | sync, 61 | branch, 62 | b: true, 63 | }), 64 | // Disable GPG signing for commits. 65 | () => gitConfigAdd({ cwd, sync, key: 'commit.gpgsign', value: false }), 66 | () => gitCreateFakeCommits({ sync, cwd, commits }), 67 | () => gitInitOrigin({ cwd, sync }), 68 | // () => gitPush({ cwd, sync }), 69 | () => format(sync as T['sync'], cwd), 70 | ) 71 | } 72 | 73 | /** 74 | * `git add .` followed by `git commit` 75 | * _Allows empty commits without any files added._ 76 | * 77 | * @param {string} cwd The CWD of the Git repository. 78 | * @param {string} message Commit message. 79 | * @returns {Promise} Promise that resolves to the SHA for the commit. 80 | */ 81 | export const gitCommitAll = ({ 82 | cwd, 83 | message, 84 | sync, 85 | }: T): TGitResult => { 86 | // Check params. 87 | // check(cwd, 'cwd: absolute') 88 | // check(message, 'message: string+') 89 | 90 | return exec( 91 | () => gitAddAll({ cwd, sync }), 92 | () => gitCommit({ cwd, message, sync: sync as T['sync'] }), 93 | ) 94 | } 95 | 96 | export interface IGitFakeRepo extends IGitInit { 97 | commits: TGitCommitDigest[] 98 | } 99 | 100 | export type TGitFakeCommitsDigest = { 101 | commits: string[] 102 | } 103 | 104 | export type TGitFakeRepoDigest = TGitFakeCommitsDigest & { 105 | cwd: string 106 | url: string 107 | } 108 | 109 | export interface IGitPushFakeCommits { 110 | cwd: string 111 | sync?: boolean 112 | commits?: TGitCommitDigest[] 113 | } 114 | 115 | export const gitCreateFakeCommits = ({ 116 | commits = [{ message: 'empty commit' }], 117 | sync, 118 | cwd, 119 | }: T): TGitResult => { 120 | const res: TGitFakeCommitsDigest = { 121 | commits: [], 122 | } 123 | 124 | return exec( 125 | ...commits.reduce( 126 | ( 127 | cb, 128 | { message = 'feat: initial commit', from, tag, branch = 'master' }, 129 | ) => { 130 | cb.push( 131 | () => gitCheckout({ cwd, sync, branch, b: true }), 132 | () => from && copyDirectory(from, cwd), 133 | () => 134 | gitCommitAll({ 135 | cwd, 136 | message, 137 | sync, 138 | }), 139 | (commit) => { 140 | res.commits.push(commit) 141 | }, 142 | (commit) => { 143 | if (tag) { 144 | return gitTag({ cwd, tag, hash: commit, sync }) 145 | } 146 | }, 147 | ) 148 | return cb 149 | }, 150 | [], 151 | ), 152 | () => format(sync as T['sync'], res), 153 | ) 154 | } 155 | 156 | export const gitPushFakeCommits = ({ 157 | commits, 158 | sync, 159 | cwd, 160 | }: T): TGitResult => { 161 | let _res: TGitFakeCommitsDigest 162 | 163 | return exec( 164 | () => gitCreateFakeCommits({ cwd, sync, commits }), 165 | (res) => { 166 | _res = res 167 | if (res.commits.length > 0) { 168 | return gitPush({ cwd, sync }) 169 | } 170 | }, 171 | () => format(sync as T['sync'], _res), 172 | ) 173 | } 174 | 175 | export const gitCreateFakeRepo = ({ 176 | sync, 177 | commits, 178 | }: T): TGitResult => { 179 | const cwd = temporaryDirectory() 180 | const res: TGitFakeRepoDigest = { 181 | cwd, 182 | url: '', 183 | commits: [], 184 | } 185 | 186 | return exec( 187 | () => gitInit({ sync, cwd }), 188 | () => gitConfigAdd({ cwd, sync, key: 'commit.gpgsign', value: false }), 189 | () => gitSetUser({ sync, cwd, name: 'Foo Bar', email: 'foo@bar.com' }), 190 | () => gitInitRemote({ sync }), 191 | (url: string) => { 192 | res.url = url 193 | }, 194 | () => gitRemoteAdd({ sync, cwd, url: res.url }), 195 | () => gitPushFakeCommits({ commits, sync, cwd }), 196 | ({ commits }) => format(sync as T['sync'], { ...res, commits }), 197 | ) 198 | } 199 | 200 | export interface IGitClone { 201 | cwd?: string 202 | sync?: boolean 203 | url: string 204 | } 205 | 206 | export const gitClone = ({ 207 | cwd = temporaryDirectory(), 208 | sync, 209 | url, 210 | }: T): TGitResult => 211 | exec( 212 | () => gitRoot(cwd, sync), 213 | (parentGitDir: string) => { 214 | if (parentGitDir) { 215 | throw new Error( 216 | `${cwd} belongs to repo ${parentGitDir as string} already`, 217 | ) 218 | } 219 | }, 220 | () => 221 | gitExec({ 222 | cwd: cwd as string, 223 | sync, 224 | args: ['clone', url, cwd], 225 | }), 226 | () => format(sync as T['sync'], cwd), 227 | ) 228 | -------------------------------------------------------------------------------- /packages/testing-suite/src/main/ts/index.ts: -------------------------------------------------------------------------------- 1 | export * from './file' 2 | export * from './git' 3 | -------------------------------------------------------------------------------- /packages/testing-suite/src/test/fixtures/basicPackage/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-basic-package", 3 | "version": "0.0.0", 4 | "private": true, 5 | "license": "0BSD", 6 | "engines": { 7 | "node": ">=8.3" 8 | }, 9 | "workspaces": [ 10 | "packages/*" 11 | ], 12 | "release": { 13 | "plugins": [ 14 | "@semantic-release/commit-analyzer", 15 | "@semantic-release/release-notes-generator" 16 | ], 17 | "noCi": true 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/testing-suite/src/test/fixtures/foo/bar/foobar.txt: -------------------------------------------------------------------------------- 1 | foobar 2 | -------------------------------------------------------------------------------- /packages/testing-suite/src/test/fixtures/foo/baz/foobaz.txt: -------------------------------------------------------------------------------- 1 | foobaz 2 | -------------------------------------------------------------------------------- /packages/testing-suite/src/test/fixtures/foo/foo/foofoo.txt: -------------------------------------------------------------------------------- 1 | foofoo 2 | -------------------------------------------------------------------------------- /packages/testing-suite/src/test/ts/it/git.ts: -------------------------------------------------------------------------------- 1 | import { dirname, resolve } from 'node:path' 2 | import { fileURLToPath } from 'node:url' 3 | 4 | import { execaSync } from 'execa' 5 | 6 | import { 7 | copyDirectory, 8 | gitAdd, 9 | gitCommit, 10 | gitCommitAll, 11 | gitGetTagHash, 12 | gitGetTags, 13 | // gitInitOrigin, 14 | gitInitTestingRepo, 15 | gitPush, 16 | gitTag, 17 | } from '../../../main/ts' 18 | 19 | const __filename = fileURLToPath(import.meta.url) 20 | const __dirname = dirname(__filename) 21 | const fixtures = resolve(__dirname, '../../fixtures') 22 | 23 | describe('gitInitOrigin()', () => { 24 | it('configures origin', () => { 25 | const sync = true 26 | const cwd = gitInitTestingRepo({ sync }) 27 | copyDirectory(`${fixtures}/basicPackage/`, cwd) 28 | const commitId = gitCommitAll({ 29 | cwd, 30 | message: 'feat: initial commit', 31 | sync, 32 | }) 33 | // const url = gitInitOrigin({ cwd, branch: 'release', sync }) 34 | // expect(url).toEqual(expect.any(String)) 35 | 36 | expect(commitId).toEqual(expect.any(String)) 37 | expect( 38 | execaSync('git', ['remote', 'show', 'origin'], { cwd }).stdout, 39 | ).toMatch(/Remote branch:\n\s+master\s+tracked/) 40 | // ).toMatch(/master\s+tracked\n\s+release\s+tracked/) 41 | }) 42 | }) 43 | 44 | describe('gitAdd()', () => { 45 | it('adds files to git', () => { 46 | const sync = true 47 | const cwd = gitInitTestingRepo({ sync }) 48 | copyDirectory(`${fixtures}/basicPackage/`, cwd) 49 | gitAdd({ cwd, sync, file: 'package.json' }) 50 | const commitId = gitCommit({ 51 | cwd, 52 | message: 'chore: add package.json', 53 | sync, 54 | }) 55 | 56 | expect(commitId).toEqual(expect.any(String)) 57 | }) 58 | }) 59 | 60 | describe('gitPush()', () => { 61 | it('pushes to remote', () => { 62 | const sync = true 63 | const cwd = gitInitTestingRepo({ sync }) 64 | copyDirectory(`${fixtures}/basicPackage/`, cwd) 65 | gitCommitAll({ cwd, message: 'feat: initial commit', sync }) 66 | 67 | expect(() => gitPush({ cwd, sync })).not.toThrowError() 68 | }) 69 | }) 70 | 71 | describe('gitTag()', () => { 72 | it('adds tag to commit', () => { 73 | const sync = true 74 | const cwd = gitInitTestingRepo({ sync }) 75 | const tag1 = 'foo@1.0.0' 76 | const tag2 = 'bar@1.0.0' 77 | copyDirectory(`${fixtures}/basicPackage/`, cwd) 78 | const commitId = gitCommitAll({ 79 | cwd, 80 | message: 'feat: initial commit', 81 | sync, 82 | }) 83 | 84 | gitTag({ cwd, tag: tag1, hash: commitId, sync }) 85 | gitTag({ cwd, tag: tag2, hash: commitId, sync }) 86 | gitPush({ cwd, sync }) 87 | 88 | const tagHash = gitGetTagHash({ cwd, tag: tag1, sync }) 89 | const tags = gitGetTags({ cwd, hash: commitId, sync }) 90 | 91 | expect(tagHash).toBe(commitId) 92 | expect(tags).toEqual([tag2, tag1]) 93 | }) 94 | }) 95 | -------------------------------------------------------------------------------- /packages/testing-suite/src/test/ts/unit/file.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'node:fs' 2 | import { dirname, resolve } from 'node:path' 3 | import { fileURLToPath } from 'node:url' 4 | 5 | import { temporaryDirectory } from 'tempy' 6 | 7 | import { cleanPath, createTestingFiles, isDirectory } from '../../../main/ts' 8 | 9 | const __filename = fileURLToPath(import.meta.url) 10 | const __dirname = dirname(__filename) 11 | const fixtures = resolve(__dirname, '../../fixtures') 12 | 13 | describe('cleanPath()', () => { 14 | it('Relative without CWD', () => { 15 | expect(cleanPath('aaa')).toBe(`${process.cwd()}/aaa`) 16 | expect(cleanPath('aaa/')).toBe(`${process.cwd()}/aaa`) 17 | }) 18 | it('Relative with CWD', () => { 19 | expect(cleanPath('ccc', '/a/b/')).toBe(`/a/b/ccc`) 20 | expect(cleanPath('ccc', '/a/b')).toBe(`/a/b/ccc`) 21 | }) 22 | it('Absolute without CWD', () => { 23 | expect(cleanPath('/aaa')).toBe(`/aaa`) 24 | expect(cleanPath('/aaa/')).toBe(`/aaa`) 25 | expect(cleanPath('/a/b/c')).toBe(`/a/b/c`) 26 | expect(cleanPath('/a/b/c/')).toBe(`/a/b/c`) 27 | }) 28 | it('Absolute with CWD', () => { 29 | expect(cleanPath('/aaa', '/x/y/z')).toBe(`/aaa`) 30 | expect(cleanPath('/aaa/', '/x/y/z')).toBe(`/aaa`) 31 | expect(cleanPath('/a/b/c', '/x/y/z')).toBe(`/a/b/c`) 32 | expect(cleanPath('/a/b/c/', '/x/y/z')).toBe(`/a/b/c`) 33 | }) 34 | }) 35 | 36 | describe('isDirectory()', () => { 37 | it('differs dirs from other dst types', () => { 38 | const cases: [string, boolean][] = [ 39 | [`${fixtures}/basicPackage`, true], 40 | [`${fixtures}/foofoo`, false], 41 | [`${fixtures}/basicPackage/package.json`, false], 42 | ] 43 | 44 | cases.forEach(([value, result]) => expect(isDirectory(value)).toBe(result)) 45 | }) 46 | }) 47 | 48 | describe('createTestingFiles', () => { 49 | it('populates cwd with specified subfolders with `test.txt`', () => { 50 | const cwd = temporaryDirectory() 51 | const folters = ['foo', 'bar'] 52 | 53 | createTestingFiles(cwd, folters) 54 | 55 | expect( 56 | readFileSync(resolve(cwd, 'foo/test.txt'), { encoding: 'utf-8' }), 57 | ).toBe('foo') 58 | expect( 59 | readFileSync(resolve(cwd, 'bar/test.txt'), { encoding: 'utf-8' }), 60 | ).toBe('bar') 61 | }) 62 | }) 63 | -------------------------------------------------------------------------------- /packages/testing-suite/src/test/ts/unit/git.ts: -------------------------------------------------------------------------------- 1 | import { dirname,resolve } from 'node:path' 2 | import {fileURLToPath} from 'node:url' 3 | 4 | import { execaSync } from 'execa' 5 | 6 | import { 7 | gitClone, 8 | gitCreateFakeRepo, 9 | gitGetTags, 10 | gitPushFakeCommits, 11 | } from '../../../main/ts' 12 | 13 | const __filename = fileURLToPath(import.meta.url) 14 | const __dirname = dirname(__filename) 15 | const fixtures = resolve(__dirname, '../../fixtures') 16 | const sync = true 17 | 18 | describe('testing suite', () => { 19 | describe('gitCreateFakeRepo()', () => { 20 | it('creates new fake repo', () => { 21 | const { cwd, url, commits } = gitCreateFakeRepo({ 22 | sync, 23 | commits: [ 24 | { 25 | message: 'chore: initial commit', 26 | from: `${fixtures}/basicPackage/`, 27 | tag: 'foobar', 28 | branch: 'foo', 29 | }, 30 | ], 31 | }) 32 | expect(cwd).toEqual(expect.any(String)) 33 | expect( 34 | execaSync('git', ['rev-parse', 'HEAD'], { cwd }).stdout, 35 | ).toEqual(commits[0]) 36 | 37 | const _cwd = gitClone({ sync, url }) 38 | 39 | execaSync('git', ['fetch', '--all'], { cwd: _cwd }) 40 | expect( 41 | execaSync('git', ['rev-parse', 'remotes/origin/foo'], { cwd: _cwd }) 42 | .stdout, 43 | ).toEqual(commits[0]) 44 | expect(gitGetTags({ cwd, hash: commits[0], sync })).toEqual(['foobar']) 45 | }) 46 | }) 47 | describe('gitPushFakeCommits()', () => { 48 | it('adds commit to repo', () => { 49 | const { cwd, url } = gitCreateFakeRepo({ 50 | sync, 51 | commits: [], 52 | }) 53 | const { commits } = gitPushFakeCommits({ 54 | cwd, 55 | sync, 56 | commits: [ 57 | { 58 | message: 'feat: foo bar baz', 59 | from: `${fixtures}/foo/`, 60 | branch: 'foo', 61 | }, 62 | { 63 | message: 'feat: initial commit', 64 | from: `${fixtures}/basicPackage/`, 65 | }, 66 | ], 67 | }) 68 | 69 | const _cwd = gitClone({ sync, url }) 70 | const _commits = execaSync('git', ['log', '-10', '--format=format:%H'], { 71 | cwd: _cwd, 72 | }) 73 | .stdout.split('\n') 74 | 75 | expect(commits).toEqual(_commits.reverse()) 76 | expect( 77 | execaSync('git', ['rev-parse', 'remotes/origin/master'], { 78 | cwd: _cwd, 79 | }).stdout, 80 | ).toEqual(commits[1]) 81 | }) 82 | }) 83 | }) 84 | -------------------------------------------------------------------------------- /packages/testing-suite/src/test/ts/unit/index.ts: -------------------------------------------------------------------------------- 1 | import { gitClone } from '../../../main/ts' 2 | 3 | describe('export', () => { 4 | it('`gitClone` is defined', () => { 5 | expect(gitClone).toEqual(expect.any(Function)) 6 | }) 7 | }) 8 | -------------------------------------------------------------------------------- /packages/testing-suite/tsconfig.es5.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "target": "es5", 5 | "outDir": "target/es5" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/testing-suite/tsconfig.es6.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "ES6", 5 | "target": "es6", 6 | "outDir": "target/es6" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/testing-suite/tsconfig.esnext.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "target": "esnext", 5 | "module": "esnext", 6 | "outDir": "target/esnext" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/testing-suite/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../packages/infra/tsconfig.compiler.json", 3 | "compilerOptions": { 4 | "rootDir": "./src/main/ts/", 5 | "baseUrl": "./src/main/ts/", 6 | "tsBuildInfoFile": "./buildcache/.tsbuildinfo" 7 | }, 8 | "references": [], 9 | "include": [ 10 | "src/main/**/*" 11 | ], 12 | "exclude": [ 13 | "node_modules" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /packages/testing-suite/tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "target": "esnext", 5 | "module": "esnext", 6 | "importHelpers": false, 7 | "noEmitHelpers": false, 8 | "esModuleInterop": false 9 | }, 10 | "include": [ 11 | "src/**/*" 12 | ], 13 | "exclude": [ 14 | "node_modules" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /packages/testing-suite/typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@qiwi/semrel-testing-suite", 3 | "out": "./docs", 4 | "exclude": ["src/test", "**/node_modules/**", "paralleljs"], 5 | "externalPattern": ["**/node_modules/**"], 6 | "excludePrivate": false, 7 | "hideGenerator": true, 8 | "readme": "README.md", 9 | "theme": "default", 10 | "tsconfig": "./tsconfig.es5.json" 11 | } 12 | -------------------------------------------------------------------------------- /packages/toolkit/README.md: -------------------------------------------------------------------------------- 1 | # @qiwi/semrel-toolkit 2 | All-in-one package to run [semantic-release](https://github.com/semantic-release/semantic-release) and [multi-semantic-release](https://github.com/qiwi/multi-semantic-release) releases with QIWI OSS plugin presets 3 | 4 | ## Usage 5 | ```shell script 6 | npx -p @qiwi/semrel-toolkit semrel 7 | npx -p @qiwi/semrel-toolkit multi-semrel 8 | ``` 9 | -------------------------------------------------------------------------------- /packages/toolkit/msr.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import('@qiwi/multi-semantic-release/bin/cli.js') 4 | -------------------------------------------------------------------------------- /packages/toolkit/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@qiwi/semrel-toolkit", 3 | "version": "3.2.11", 4 | "publishConfig": { 5 | "access": "public" 6 | }, 7 | "description": "Semantic release tools, plugins and configs for QIWI OSS projects", 8 | "keywords": [], 9 | "files": [ 10 | "README.md", 11 | "CHANGELOG.md", 12 | "msr.js", 13 | "semrel.js" 14 | ], 15 | "bin": { 16 | "multi-semrel": "./msr.js", 17 | "semrel": "./semrel.js" 18 | }, 19 | "dependencies": { 20 | "@qiwi/multi-semantic-release": "~6.5.1", 21 | "@qiwi/semrel-config": "workspace:*", 22 | "@qiwi/semrel-config-monorepo": "workspace:*", 23 | "@qiwi/semrel-preset": "workspace:*", 24 | "semantic-release": "~19.0.5" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "git+https://github.com/qiwi/semantic-release-toolkit.git" 29 | }, 30 | "author": "Anton Golub ", 31 | "license": "MIT", 32 | "bugs": { 33 | "url": "https://github.com/qiwi/semantic-release-toolkit/issues" 34 | }, 35 | "homepage": "https://github.com/qiwi/semantic-release-toolkit/#readme" 36 | } 37 | -------------------------------------------------------------------------------- /packages/toolkit/semrel.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('semantic-release/bin/semantic-release.js') 4 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ], 5 | "automerge": true, 6 | "automergeType": "pr", 7 | "rangeStrategy": "replace" 8 | } 9 | -------------------------------------------------------------------------------- /scripts/js/coverage-merge.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const findGitRoot = require('find-git-root') 4 | const { resolve } = require('path') 5 | 6 | const { 7 | mkdirSync, 8 | existsSync, 9 | readFileSync, 10 | readdirSync, 11 | writeFileSync 12 | } = require('fs') 13 | 14 | const ROOT = resolve(findGitRoot(), '..') 15 | const PACKAGES_ROOT = resolve(ROOT, 'packages') 16 | const COV_DIR = 'coverage' 17 | const COV_FINAL = 'coverage-final.json' 18 | const LCOV = 'lcov.info' 19 | 20 | const getDirectories = source => 21 | readdirSync(source, { withFileTypes: true }) 22 | .filter(dirent => dirent.isDirectory()) 23 | .map(dirent => resolve(source, dirent.name)) 24 | 25 | const read = (file, cb) => { 26 | if (existsSync(file)) { 27 | const data = readFileSync(file, { encoding: 'utf-8' }) 28 | 29 | try { 30 | cb(data) 31 | 32 | } catch (e) { 33 | console.warn(e) 34 | } 35 | } 36 | } 37 | 38 | const write = (dir, name, data) => { 39 | mkdirSync(dir, { recursive: true }) 40 | writeFileSync(resolve(dir, name), data) 41 | } 42 | 43 | const mergeCoverage = () => { 44 | let lcov = '' 45 | const covFinal = {} 46 | const packages = getDirectories(PACKAGES_ROOT) 47 | 48 | packages.forEach(pack => { 49 | read(resolve(pack, COV_DIR, COV_FINAL), data => Object.assign(covFinal, JSON.parse(data))) 50 | read(resolve(pack, COV_DIR, LCOV), data => {lcov = lcov + data}) 51 | }) 52 | 53 | write(resolve(ROOT, COV_DIR), LCOV, lcov) 54 | write(resolve(ROOT, COV_DIR), COV_FINAL, JSON.stringify(covFinal)) 55 | } 56 | 57 | mergeCoverage() 58 | -------------------------------------------------------------------------------- /scripts/sh/patch-pkg-main.sh: -------------------------------------------------------------------------------- 1 | packages="node_modules/tslib/package.json packages/plugin-creator/package.json packages/metabranch/package.json" 2 | 3 | if [ $# -eq 1 ]; then 4 | for p in $packages; 5 | do jq 'if (.main) then ._main = .main | del(.main) else . end' $p | sponge $p; 6 | done; 7 | echo "package.json main disabled: https://github.com/facebook/jest/pull/11961"; 8 | else 9 | for p in $packages; 10 | do jq 'if (._main) then .main = ._main | del(._main) else . end' $p | sponge $p; 11 | done; 12 | echo "package.json main restored: https://github.com/facebook/jest/pull/11961"; 13 | fi 14 | -------------------------------------------------------------------------------- /scripts/sh/update.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | NPM_UPGRADE="npm-upgrade" 4 | PACKAGES=$(cat package.json | jq -r '.workspaces | join(" ")') 5 | 6 | eval $NPM_UPGRADE 7 | 8 | for f in $PACKAGES; do 9 | if [ -d "$f" ]; then 10 | cd $f 11 | eval $NPM_UPGRADE 12 | fi 13 | done 14 | -------------------------------------------------------------------------------- /tsconfig.esnext.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "references": [ 4 | { "path": "./packages/common/tsconfig.esnext.json" }, 5 | { "path": "./packages/plugin-creator/tsconfig.esnext.json" }, 6 | { "path": "./packages/plugin-actions/tsconfig.es6.json" }, 7 | { "path": "./packages/metabranch/tsconfig.esnext.json" }, 8 | { "path": "./packages/testing-suite/tsconfig.esnext.json" }, 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "temp/prebuild", 4 | "emitDeclarationOnly": true, 5 | "esModuleInterop": true, 6 | "importHelpers": false, 7 | "noEmitHelpers": true, 8 | "declaration": true, 9 | "module": "ESNext", 10 | "moduleResolution": "Node" 11 | }, 12 | "include": [ 13 | "./packages/*/src/main/**/*" 14 | ], 15 | "references": [ 16 | { "path": "./packages/common/tsconfig.es6.json" }, 17 | { "path": "./packages/plugin-creator/tsconfig.es6.json" }, 18 | { "path": "./packages/plugin-actions/tsconfig.es6.json" }, 19 | { "path": "./packages/metabranch/tsconfig.es6.json" }, 20 | { "path": "./packages/testing-suite/tsconfig.es6.json" }, 21 | ] 22 | } 23 | --------------------------------------------------------------------------------