├── .github ├── FUNDING.yml └── workflows │ └── example.yml ├── LICENSE ├── README.md └── action.yml /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: JamieMason 2 | -------------------------------------------------------------------------------- /.github/workflows/example.yml: -------------------------------------------------------------------------------- 1 | on: 2 | # Run whenever a pull request is updated 3 | pull_request: 4 | branches: 5 | - main 6 | jobs: 7 | syncpack: 8 | runs-on: ubuntu-latest 9 | name: syncpack 10 | steps: 11 | # Check out the branch so we can read/write its files 12 | - uses: actions/checkout@v3 13 | # Use Node.js as syncpack is written in that 14 | - uses: actions/setup-node@v3 15 | with: 16 | node-version: 16 17 | - uses: JamieMason/syncpack-github-action@0.2.3 18 | env: 19 | # If any options are set to 'fix' then 20 | # env.GITHUB_TOKEN is needed to push to PRs 21 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 22 | with: 23 | # Email to be used when the GitHub Action Commits and Comments 24 | bot-email: 'github-actions@github.com' 25 | # GitHub Username to be used when the GitHub Action Commits and 26 | # Comments 27 | bot-username: 'github-actions[bot]' 28 | # Commit message to be used when auto-fixing PRs 29 | commit-message: 'chore(syncpack): format and sync package.json files' 30 | # How to handle unconventional formatting/ordering of package.json 31 | # files 32 | # 33 | # * 'fix' - Auto-fix and commit to the PR 34 | # * 'ignore' - Do nothing 35 | format-mismatches: 'fix' 36 | # Whether to update package-lock.json, pnpm-lock.yaml, or yarn.lock 37 | # when semver range or version mismatches have been auto-fixed 38 | # 39 | # * 'fix' - Auto-fix and commit to the PR 40 | # * 'ignore' - Do nothing 41 | lockfile-mismatches: 'fix' 42 | # The specific version of your chosen `package-manager` 43 | package-manager-version: 'latest' 44 | # What to use to update your lockfile 45 | # 46 | # * 'npm' 47 | # * 'pnpm' 48 | # * 'yarn' 49 | package-manager: 'npm' 50 | # Ensure dependency versions follow a consistent format 51 | # 52 | # * 'lint' - Block merging the PR until manually fixed 53 | # * 'fix' - Auto-fix and commit to the PR 54 | # * 'ignore' - Do nothing 55 | semver-range-mismatches: 'fix' 56 | # Migrate to a newer version of syncpack than was available at the 57 | # time of writing this GitHub action 58 | syncpack-version: '7.2.1' 59 | # How to handle dependencies which are required by multiple packages, 60 | # where the version is not the same across every package 61 | # 62 | # * 'lint' - Block merging the PR until manually fixed 63 | # * 'fix' - Auto-fix and commit to the PR 64 | # * 'ignore' - Do nothing 65 | version-mismatches: 'fix' 66 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Jamie Mason 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 | # syncpack-github-action 2 | 3 | A GitHub Action to Synchronise monorepo dependency versions with [`syncpack`](https://github.com/JamieMason/syncpack). 4 | 5 | ## ⚠️ Project Status 6 | 7 | [`syncpack`](https://github.com/JamieMason/syncpack) is stable but this GitHub Action is very new (May 15th 2022). 8 | 9 | ## 🌩 Installation 10 | 11 | 1. Create a [`.syncpackrc`](https://github.com/JamieMason/syncpack#-configuration-file) file in the root of your project. 12 | ```json 13 | { 14 | "dev": true, 15 | "filter": ".", 16 | "indent": " ", 17 | "overrides": true, 18 | "peer": true, 19 | "prod": true, 20 | "resolutions": true, 21 | "workspace": true, 22 | "semverGroups": [], 23 | "semverRange": "", 24 | "sortAz": [ 25 | "contributors", 26 | "dependencies", 27 | "devDependencies", 28 | "keywords", 29 | "peerDependencies", 30 | "resolutions", 31 | "scripts" 32 | ], 33 | "sortFirst": ["name", "description", "version", "author"], 34 | "source": [], 35 | "versionGroups": [] 36 | } 37 | ``` 38 | 1. Create a `/.github/workflows/syncpack.yml` workflow to run syncpack on Pull Requests. 39 | 40 | ```yaml 41 | on: 42 | # Run whenever a pull request is updated 43 | pull_request: 44 | branches: 45 | - main 46 | jobs: 47 | syncpack: 48 | runs-on: ubuntu-latest 49 | name: syncpack 50 | steps: 51 | # Check out the branch so we can read/write its files 52 | - uses: actions/checkout@v3 53 | # Use Node.js as syncpack is written in that 54 | - uses: actions/setup-node@v3 55 | with: 56 | node-version: 16 57 | - uses: JamieMason/syncpack-github-action@0.2.3 58 | env: 59 | # If any options are set to 'fix' then 60 | # env.GITHUB_TOKEN is needed to push to PRs 61 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 62 | ``` 63 | 64 | 1. Unless you don't plan to use `fix` to commit automatic fixes to your Pull Requests, go to `/settings/actions` in your repository and enable **Read and write permissions** under **Workflow permissions**. 65 | 66 | ## 🛠 Settings 67 | 68 | All settings are optional, and their default values are listed below. 69 | 70 | ```yaml 71 | with: 72 | # Email to be used when the GitHub Action Commits and Comments 73 | bot-email: 'github-actions@github.com' 74 | # GitHub Username to be used when the GitHub Action Commits and 75 | # Comments 76 | bot-username: 'github-actions[bot]' 77 | # Commit message to be used when auto-fixing PRs 78 | commit-message: 'chore(syncpack): format and sync package.json files' 79 | # How to handle unconventional formatting/ordering of package.json 80 | # files 81 | # 82 | # * 'fix' - Auto-fix and commit to the PR 83 | # * 'ignore' - Do nothing 84 | format-mismatches: 'fix' 85 | # Whether to update package-lock.json, pnpm-lock.yaml, or yarn.lock 86 | # when semver range or version mismatches have been auto-fixed 87 | # 88 | # * 'fix' - Auto-fix and commit to the PR 89 | # * 'ignore' - Do nothing 90 | lockfile-mismatches: 'fix' 91 | # The specific version of your chosen `package-manager` 92 | package-manager-version: 'latest' 93 | # What to use to update your lockfile 94 | # 95 | # * 'npm' 96 | # * 'pnpm' 97 | # * 'yarn' 98 | package-manager: 'npm' 99 | # Ensure dependency versions follow a consistent format 100 | # 101 | # * 'lint' - Block merging the PR until manually fixed 102 | # * 'fix' - Auto-fix and commit to the PR 103 | # * 'ignore' - Do nothing 104 | semver-range-mismatches: 'fix' 105 | # Migrate to a newer version of syncpack than was available at the 106 | # time of writing this GitHub action 107 | syncpack-version: '7.2.1' 108 | # How to handle dependencies which are required by multiple packages, 109 | # where the version is not the same across every package 110 | # 111 | # * 'lint' - Block merging the PR until manually fixed 112 | # * 'fix' - Auto-fix and commit to the PR 113 | # * 'ignore' - Do nothing 114 | version-mismatches: 'fix' 115 | ``` 116 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: 'Syncpack: Synchronise monorepo dependency versions' 2 | description: 'Always use the same versions of each dependency' 3 | 4 | branding: 5 | icon: 'cloud-lightning' 6 | color: 'green' 7 | 8 | inputs: 9 | bot-email: 10 | description: 'GitHub Email for the Syncpack Bot' 11 | required: false 12 | default: 'github-actions@github.com' 13 | bot-username: 14 | description: 'GitHub Username for the Syncpack Bot' 15 | required: false 16 | default: 'github-actions[bot]' 17 | commit-message: 18 | description: 'Commit Message to use when applying fixes' 19 | required: false 20 | default: 'chore(syncpack): format and sync package.json files' 21 | format-mismatches: 22 | description: '"fix" | "ignore"' 23 | required: false 24 | default: 'fix' 25 | lockfile-mismatches: 26 | description: '"fix" | "ignore"' 27 | required: false 28 | default: 'fix' 29 | package-manager-version: 30 | description: 'The Semver version of npm, yarn, or pnpm' 31 | required: false 32 | default: 'latest' 33 | package-manager: 34 | description: 'Possible values npm | yarn | pnpm' 35 | required: false 36 | default: 'npm' 37 | semver-range-mismatches: 38 | description: '"lint" | "fix" | "ignore"' 39 | required: false 40 | default: 'fix' 41 | syncpack-version: 42 | description: 'The Semver version of syncpack' 43 | required: false 44 | default: '7.2.1' 45 | version-mismatches: 46 | description: '"lint" | "fix" | "ignore"' 47 | required: false 48 | default: 'fix' 49 | 50 | runs: 51 | using: 'composite' 52 | steps: 53 | - name: Init 54 | id: init 55 | shell: bash 56 | run: | 57 | if [ "${{ inputs.version-mismatches }}" = "fix" ] || [ "${{ inputs.format-mismatches }}" = "fix" ] || [ "${{ inputs.semver-range-mismatches }}" = "fix" ]; then 58 | echo "anyFixEnabled=true" >> $GITHUB_OUTPUT 59 | fi 60 | 61 | - name: Install Syncpack 62 | id: install-syncpack 63 | shell: bash 64 | run: | 65 | npm i -g "syncpack@${{ inputs.syncpack-version }}" 66 | 67 | - if: ${{ steps.init.outputs.anyFixEnabled == 'true' }} 68 | name: Checkout Branch 69 | id: checkout-branch 70 | shell: bash 71 | run: | 72 | git config pull.rebase true 73 | git fetch 74 | git checkout $GITHUB_HEAD_REF 75 | git pull origin $GITHUB_HEAD_REF 76 | 77 | # Version Mismatches 78 | # https://github.com/JamieMason/syncpack#list-mismatches 79 | # https://github.com/JamieMason/syncpack#fix-mismatches 80 | 81 | - if: ${{ inputs.version-mismatches == 'fix' }} 82 | name: syncpack fix-mismatches 83 | id: version-mismatches-fix 84 | shell: bash 85 | run: | 86 | set +e 87 | value="$(syncpack fix-mismatches)" 88 | value="${value//'%'/'%25'}" 89 | value="${value//$'\n'/'%0A'}" 90 | value="${value//$'\r'/'%0D'}" 91 | echo "stdout=$value" >> $GITHUB_OUTPUT 92 | set -e 93 | if [[ `git diff --name-only` ]]; then 94 | git add . 95 | echo "has-changes=true" >> $GITHUB_OUTPUT 96 | fi 97 | 98 | # Format Mismatches 99 | # https://github.com/JamieMason/syncpack#format 100 | 101 | - if: ${{ inputs.format-mismatches == 'fix' }} 102 | name: syncpack format 103 | id: format-mismatches-fix 104 | shell: bash 105 | run: | 106 | set +e 107 | value="$(syncpack format)" 108 | value="${value//'%'/'%25'}" 109 | value="${value//$'\n'/'%0A'}" 110 | value="${value//$'\r'/'%0D'}" 111 | echo "stdout=$value" >> $GITHUB_OUTPUT 112 | set -e 113 | if [[ `git diff --name-only` ]]; then 114 | git add . 115 | echo "has-changes=true" >> $GITHUB_OUTPUT 116 | fi 117 | 118 | # Semver Range Mismatches 119 | # https://github.com/JamieMason/syncpack#lint-semver-ranges 120 | # https://github.com/JamieMason/syncpack#set-semver-ranges 121 | 122 | - if: ${{ inputs.semver-range-mismatches == 'fix' }} 123 | name: syncpack set-semver-ranges 124 | id: semver-range-mismatches-fix 125 | shell: bash 126 | run: | 127 | set +e 128 | value="$(syncpack set-semver-ranges)" 129 | value="${value//'%'/'%25'}" 130 | value="${value//$'\n'/'%0A'}" 131 | value="${value//$'\r'/'%0D'}" 132 | echo "stdout=$value" >> $GITHUB_OUTPUT 133 | set -e 134 | if [[ `git diff --name-only` ]]; then 135 | git add . 136 | echo "has-changes=true" >> $GITHUB_OUTPUT 137 | fi 138 | 139 | - if: ${{ inputs.lockfile-mismatches != 'ignore' }} 140 | name: Update Lockfile 141 | id: lockfile-mismatches-fix 142 | shell: bash 143 | run: | 144 | if [ "${{ inputs.package-manager }}" = "yarn" ]; then 145 | npm i -g "yarn@${{ inputs.package-manager-version }}" 146 | yarn install 147 | elif [ "${{ inputs.package-manager }}" = "pnpm" ]; then 148 | npm i -g "pnpm@${{ inputs.package-manager-version }}" 149 | pnpm install --no-frozen-lockfile 150 | else 151 | npm i -g "npm@${{ inputs.package-manager-version }}" 152 | npm install 153 | fi 154 | 155 | - if: ${{ steps.init.outputs.anyFixEnabled == 'true' }} 156 | name: Detect Changes 157 | id: detect-changes 158 | shell: bash 159 | run: | 160 | if [[ `git status --porcelain` ]]; then 161 | echo "has-changes=true" >> $GITHUB_OUTPUT 162 | fi 163 | 164 | - if: ${{ steps.init.outputs.anyFixEnabled == 'true' }} 165 | name: Commit and Push 166 | id: commit-and-push 167 | shell: bash 168 | run: | 169 | if [ "${{ steps.detect-changes.outputs.has-changes }}" = "true" ]; then 170 | git add . 171 | git config user.name "${{ inputs.bot-username }}" 172 | git config user.email "${{ inputs.bot-email }}" 173 | git commit -m "${{ inputs.commit-message }}" 174 | git push origin HEAD:"$GITHUB_HEAD_REF" 175 | git push 176 | fi 177 | 178 | - if: always() 179 | name: syncpack list-mismatches 180 | id: version-mismatches-lint 181 | shell: bash 182 | run: | 183 | if [ "${{ inputs.version-mismatches }}" = "lint" ]; then 184 | set +e 185 | value="$(syncpack list-mismatches)" 186 | value="${value//'%'/'%25'}" 187 | value="${value//$'\n'/'%0A'}" 188 | value="${value//$'\r'/'%0D'}" 189 | echo "stdout=$value" >> $GITHUB_OUTPUT 190 | set -e 191 | syncpack list-mismatches 192 | fi 193 | 194 | - if: always() 195 | name: syncpack lint-semver-ranges 196 | id: semver-range-mismatches-lint 197 | shell: bash 198 | run: | 199 | if [ "${{ inputs.semver-range-mismatches }}" = "lint" ]; then 200 | set +e 201 | value="$(syncpack lint-semver-ranges)" 202 | value="${value//'%'/'%25'}" 203 | value="${value//$'\n'/'%0A'}" 204 | value="${value//$'\r'/'%0D'}" 205 | echo "stdout=$value" >> $GITHUB_OUTPUT 206 | set -e 207 | syncpack lint-semver-ranges 208 | fi 209 | 210 | - if: always() 211 | name: Get Comment Body 212 | id: get-comment-body 213 | shell: bash 214 | run: | 215 | touch comment.txt 216 | if [ "${{ inputs.version-mismatches }}" = "lint" ]; then 217 | if [ "${{ steps.version-mismatches-lint.outcome }}" = "success" ]; then 218 | echo '```' >> comment.txt 219 | echo "$ syncpack list-mismatches" >> comment.txt 220 | echo "✓" >> comment.txt 221 | echo '```' >> comment.txt 222 | echo "" >> comment.txt 223 | fi 224 | if [ "${{ steps.version-mismatches-lint.outcome }}" = "failure" ]; then 225 | echo "" >> comment.txt 226 | echo '```' >> comment.txt 227 | echo "$ syncpack list-mismatches" >> comment.txt 228 | echo "${{ steps.version-mismatches-lint.outputs.stdout }}" >> comment.txt 229 | echo '```' >> comment.txt 230 | echo "" >> comment.txt 231 | fi 232 | fi 233 | if [ "${{ inputs.semver-range-mismatches }}" = "lint" ]; then 234 | if [ "${{ steps.semver-range-mismatches-lint.outcome }}" = "success" ]; then 235 | echo '```' >> comment.txt 236 | echo "$ syncpack lint-semver-ranges" >> comment.txt 237 | echo "✓" >> comment.txt 238 | echo '```' >> comment.txt 239 | echo "" >> comment.txt 240 | fi 241 | if [ "${{ steps.semver-range-mismatches-lint.outcome }}" = "failure" ]; then 242 | echo "" >> comment.txt 243 | echo '```' >> comment.txt 244 | echo "$ syncpack lint-semver-ranges" >> comment.txt 245 | echo "${{ steps.semver-range-mismatches-lint.outputs.stdout }}" >> comment.txt 246 | echo '```' >> comment.txt 247 | echo "" >> comment.txt 248 | fi 249 | fi 250 | if [ "${{ inputs.version-mismatches }}" = "fix" ]; then 251 | if [ "${{ steps.version-mismatches-fix.outputs.has-changes }}" = "true" ]; then 252 | echo "" >> comment.txt 253 | echo '```' >> comment.txt 254 | echo "$ syncpack fix-mismatches" >> comment.txt 255 | echo "${{ steps.version-mismatches-fix.outputs.stdout }}" >> comment.txt 256 | echo '```' >> comment.txt 257 | echo "" >> comment.txt 258 | fi 259 | fi 260 | if [ "${{ inputs.format-mismatches }}" = "fix" ]; then 261 | if [ "${{ steps.format-mismatches-fix.outputs.has-changes }}" = "true" ]; then 262 | echo "" >> comment.txt 263 | echo '```' >> comment.txt 264 | echo "$ syncpack format" >> comment.txt 265 | echo "${{ steps.format-mismatches-fix.outputs.stdout }}" >> comment.txt 266 | echo '```' >> comment.txt 267 | echo "" >> comment.txt 268 | fi 269 | fi 270 | if [ "${{ inputs.semver-range-mismatches }}" = "fix" ]; then 271 | if [ "${{ steps.semver-range-mismatches-fix.outputs.has-changes }}" = "true" ]; then 272 | echo "" >> comment.txt 273 | echo '```' >> comment.txt 274 | echo "$ syncpack set-semver-ranges" >> comment.txt 275 | echo "${{ steps.semver-range-mismatches-fix.outputs.stdout }}" >> comment.txt 276 | echo '```' >> comment.txt 277 | echo "" >> comment.txt 278 | fi 279 | fi 280 | body=$(cat comment.txt) 281 | body="${body//'%'/'%25'}" 282 | body="${body//$'\n'/'%0A'}" 283 | body="${body//$'\r'/'%0D'}" 284 | echo "body="$body"" >> $GITHUB_OUTPUT 285 | 286 | - if: always() 287 | name: Find Comment 288 | id: find-comment 289 | uses: peter-evans/find-comment@v2 290 | with: 291 | issue-number: ${{ github.event.pull_request.number }} 292 | comment-author: ${{ inputs.bot-username }} 293 | body-includes: '' 294 | 295 | - if: always() 296 | name: Upsert Comment 297 | id: upsert-comment 298 | uses: peter-evans/create-or-update-comment@v2 299 | with: 300 | comment-id: ${{ steps.find-comment.outputs.comment-id }} 301 | issue-number: ${{ github.event.pull_request.number }} 302 | edit-mode: replace 303 | body: | 304 | Synchronise monorepo dependency versions with [`syncpack`](https://github.com/JamieMason/syncpack) 305 | ${{ steps.get-comment-body.outputs.body }} 306 | 307 | --------------------------------------------------------------------------------