├── .all-contributorsrc ├── .github ├── dependabot.yml ├── release.yml └── workflows │ ├── auto-merge-dependabot.yml │ ├── ci.yml │ ├── code_coverage_comment.yml │ ├── release_auto.yml │ ├── release_pr_snapshot.yml │ └── remove_snapshot_branch.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── action.yml ├── package-lock.json ├── package.json ├── src ├── action │ ├── main.ts │ ├── post.ts │ └── pre.ts ├── core │ ├── cache.ts │ ├── files.ts │ ├── http.ts │ ├── logger.ts │ ├── os.ts │ └── types.ts ├── modules │ ├── coursier.ts │ ├── github.test.ts │ ├── github.ts │ ├── healthcheck.test.ts │ ├── healthcheck.ts │ ├── input.test.ts │ ├── input.ts │ ├── mill.ts │ ├── workspace.test.ts │ └── workspace.ts └── utils │ └── docs.ts └── tsconfig.json /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "scala-steward-action", 3 | "projectOwner": "scala-steward-org", 4 | "repoType": "github", 5 | "repoHost": "https://github.com", 6 | "files": [ 7 | "README.md" 8 | ], 9 | "imageSize": 100, 10 | "commit": true, 11 | "commitConvention": "none", 12 | "contributorsSortAlphabetically": "true", 13 | "contributors": [ 14 | { 15 | "login": "alejandrohdezma", 16 | "name": "Alejandro Hernández", 17 | "avatar_url": "https://avatars.githubusercontent.com/u/9027541?v=4", 18 | "profile": "https://alejandrohdezma.com/", 19 | "contributions": [ 20 | "code" 21 | ] 22 | }, 23 | { 24 | "login": "laughedelic", 25 | "name": "Alexey Alekhin", 26 | "avatar_url": "https://avatars.githubusercontent.com/u/766656?v=4", 27 | "profile": "https://github.com/laughedelic", 28 | "contributions": [ 29 | "code" 30 | ] 31 | }, 32 | { 33 | "login": "fthomas", 34 | "name": "Frank Thomas", 35 | "avatar_url": "https://avatars.githubusercontent.com/u/141252?v=4", 36 | "profile": "https://github.com/fthomas", 37 | "contributions": [ 38 | "code" 39 | ] 40 | }, 41 | { 42 | "login": "ryota0624", 43 | "name": "ryota0624", 44 | "avatar_url": "https://avatars.githubusercontent.com/u/11390724?v=4", 45 | "profile": "https://github.com/ryota0624", 46 | "contributions": [ 47 | "code" 48 | ] 49 | }, 50 | { 51 | "login": "arashi01", 52 | "name": "Ali Salim Rashid", 53 | "avatar_url": "https://avatars.githubusercontent.com/u/1921493?v=4", 54 | "profile": "https://github.com/arashi01", 55 | "contributions": [ 56 | "code" 57 | ] 58 | }, 59 | { 60 | "login": "xuwei-k", 61 | "name": "kenji yoshida", 62 | "avatar_url": "https://avatars.githubusercontent.com/u/389787?v=4", 63 | "profile": "https://twitter.com/xuwei_k", 64 | "contributions": [ 65 | "code", 66 | "question" 67 | ] 68 | }, 69 | { 70 | "login": "EwoutH", 71 | "name": "Ewout ter Hoeven", 72 | "avatar_url": "https://avatars.githubusercontent.com/u/15776622?v=4", 73 | "profile": "https://github.com/EwoutH", 74 | "contributions": [ 75 | "code" 76 | ] 77 | }, 78 | { 79 | "login": "MPV", 80 | "name": "Victor Sollerhed", 81 | "avatar_url": "https://avatars.githubusercontent.com/u/62675?v=4", 82 | "profile": "http://victor.sollerhed.se/", 83 | "contributions": [ 84 | "code" 85 | ] 86 | }, 87 | { 88 | "login": "NomadBlacky", 89 | "name": "Takumi Kadowaki", 90 | "avatar_url": "https://avatars.githubusercontent.com/u/3215961?v=4", 91 | "profile": "https://www.nomadblacky.dev/", 92 | "contributions": [ 93 | "code" 94 | ] 95 | }, 96 | { 97 | "login": "TonioGela", 98 | "name": "Antonio Gelameris", 99 | "avatar_url": "https://avatars.githubusercontent.com/u/41690956?v=4", 100 | "profile": "https://toniogela.dev/", 101 | "contributions": [ 102 | "code" 103 | ] 104 | }, 105 | { 106 | "login": "bpg", 107 | "name": "Pavel Boldyrev", 108 | "avatar_url": "https://avatars.githubusercontent.com/u/627562?v=4", 109 | "profile": "http://ca.linkedin.com/in/pboldyrev/", 110 | "contributions": [ 111 | "code" 112 | ] 113 | }, 114 | { 115 | "login": "tovbinm", 116 | "name": "Matthew Tovbin", 117 | "avatar_url": "https://avatars.githubusercontent.com/u/629845?v=4", 118 | "profile": "https://www.tovbin.com/", 119 | "contributions": [ 120 | "code" 121 | ] 122 | }, 123 | { 124 | "login": "michele-pinto-kensu", 125 | "name": "Michele Pinto", 126 | "avatar_url": "https://avatars.githubusercontent.com/u/69146696?v=4", 127 | "profile": "https://github.com/michele-pinto-kensu", 128 | "contributions": [ 129 | "ideas" 130 | ] 131 | }, 132 | { 133 | "login": "spliakos", 134 | "name": "Stefanos Pliakos", 135 | "avatar_url": "https://avatars.githubusercontent.com/u/15560159?v=4", 136 | "profile": "https://github.com/spliakos", 137 | "contributions": [ 138 | "ideas" 139 | ] 140 | }, 141 | { 142 | "login": "jshiell", 143 | "name": "Jamie Shiell", 144 | "avatar_url": "https://avatars.githubusercontent.com/u/1030482?v=4", 145 | "profile": "https://infernus.org/", 146 | "contributions": [ 147 | "bug" 148 | ] 149 | }, 150 | { 151 | "login": "marcelocarlos", 152 | "name": "Marcelo Carlos", 153 | "avatar_url": "https://avatars.githubusercontent.com/u/16080771?v=4", 154 | "profile": "https://github.com/marcelocarlos", 155 | "contributions": [ 156 | "bug" 157 | ] 158 | }, 159 | { 160 | "login": "jeffboutotte", 161 | "name": "Jeff Boutotte", 162 | "avatar_url": "https://avatars.githubusercontent.com/u/6991403?v=4", 163 | "profile": "https://github.com/jeffboutotte", 164 | "contributions": [ 165 | "code" 166 | ] 167 | }, 168 | { 169 | "login": "milanvdm", 170 | "name": "Milan van der Meer", 171 | "avatar_url": "https://avatars.githubusercontent.com/u/5628925?v=4", 172 | "profile": "https://github.com/milanvdm", 173 | "contributions": [ 174 | "bug" 175 | ] 176 | }, 177 | { 178 | "login": "exoego", 179 | "name": "TATSUNO Yasuhiro", 180 | "avatar_url": "https://avatars.githubusercontent.com/u/127635?v=4", 181 | "profile": "https://www.exoego.net/", 182 | "contributions": [ 183 | "code" 184 | ] 185 | }, 186 | { 187 | "login": "francisdb", 188 | "name": "Francis De Brabandere", 189 | "avatar_url": "https://avatars.githubusercontent.com/u/161305?v=4", 190 | "profile": "https://github.com/francisdb", 191 | "contributions": [ 192 | "bug" 193 | ] 194 | }, 195 | { 196 | "login": "armanbilge", 197 | "name": "Arman Bilge", 198 | "avatar_url": "https://avatars.githubusercontent.com/u/3119428?v=4", 199 | "profile": "https://github.com/armanbilge", 200 | "contributions": [ 201 | "bug", 202 | "code" 203 | ] 204 | }, 205 | { 206 | "login": "ybasket", 207 | "name": "Yannick Heiber", 208 | "avatar_url": "https://avatars.githubusercontent.com/u/2632023?v=4", 209 | "profile": "https://github.com/ybasket", 210 | "contributions": [ 211 | "code", 212 | "bug" 213 | ] 214 | }, 215 | { 216 | "login": "wunderk1nd-e", 217 | "name": "Elias Court", 218 | "avatar_url": "https://avatars.githubusercontent.com/u/36158087?v=4", 219 | "profile": "https://k1nd.ltd/", 220 | "contributions": [ 221 | "code" 222 | ] 223 | }, 224 | { 225 | "login": "leobenkel", 226 | "name": "Leo Benkel", 227 | "avatar_url": "https://avatars.githubusercontent.com/u/4960573?v=4", 228 | "profile": "https://leobenkel.com/", 229 | "contributions": [ 230 | "bug" 231 | ] 232 | }, 233 | { 234 | "login": "anatoliykmetyuk", 235 | "name": "Anatolii Kmetiuk", 236 | "avatar_url": "https://avatars.githubusercontent.com/u/2614813?v=4", 237 | "profile": "https://akmetiuk.com/", 238 | "contributions": [ 239 | "doc" 240 | ] 241 | }, 242 | { 243 | "login": "jyrkih", 244 | "name": "Jyrki Hokkanen", 245 | "avatar_url": "https://avatars.githubusercontent.com/u/2580851?v=4", 246 | "profile": "https://github.com/jyrkih", 247 | "contributions": [ 248 | "bug" 249 | ] 250 | }, 251 | { 252 | "login": "yokra9", 253 | "name": "yokra", 254 | "avatar_url": "https://avatars.githubusercontent.com/u/53964890?v=4", 255 | "profile": "https://qiita.com/yokra9", 256 | "contributions": [ 257 | "doc" 258 | ] 259 | }, 260 | { 261 | "login": "ckipp01", 262 | "name": "Chris Kipp", 263 | "avatar_url": "https://avatars.githubusercontent.com/u/13974112?v=4", 264 | "profile": "https://chris-kipp.io/", 265 | "contributions": [ 266 | "bug", 267 | "code" 268 | ] 269 | }, 270 | { 271 | "login": "regadas", 272 | "name": "Filipe Regadas", 273 | "avatar_url": "https://avatars.githubusercontent.com/u/163899?v=4", 274 | "profile": "https://regadas.dev/", 275 | "contributions": [ 276 | "doc" 277 | ] 278 | }, 279 | { 280 | "login": "fmeriaux", 281 | "name": "Florian Meriaux", 282 | "avatar_url": "https://avatars.githubusercontent.com/u/16759768?v=4", 283 | "profile": "https://github.com/fmeriaux", 284 | "contributions": [ 285 | "bug" 286 | ] 287 | }, 288 | { 289 | "login": "adamw", 290 | "name": "Adam Warski", 291 | "avatar_url": "https://avatars.githubusercontent.com/u/60503?v=4", 292 | "profile": "http://www.warski.org/", 293 | "contributions": [ 294 | "question" 295 | ] 296 | }, 297 | { 298 | "login": "SpecialThing44", 299 | "name": "Spencer Perkins", 300 | "avatar_url": "https://avatars.githubusercontent.com/u/95900100?v=4", 301 | "profile": "https://github.com/SpecialThing44", 302 | "contributions": [ 303 | "doc" 304 | ] 305 | } 306 | ], 307 | "contributorsPerLine": 6 308 | } 309 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "monthly" 7 | - package-ecosystem: npm 8 | directory: "/" 9 | schedule: 10 | interval: monthly 11 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | exclude: 3 | labels: 4 | - ":chart_with_upwards_trend: dependency-update" 5 | authors: 6 | - dependabot 7 | categories: 8 | - title: "⚠️ Breaking changes" 9 | labels: 10 | - ":warning: breaking" 11 | - title: "🚀 New features" 12 | labels: 13 | - ":rocket: feature" 14 | - title: "📘 Documentation updates" 15 | labels: 16 | - ":blue_book: documentation" 17 | - title: "🐛 Bug fixes" 18 | labels: 19 | - ":beetle: bug" 20 | - title: Other Changes 21 | labels: 22 | - "*" 23 | -------------------------------------------------------------------------------- /.github/workflows/auto-merge-dependabot.yml: -------------------------------------------------------------------------------- 1 | name: Auto-merge Dependabot PR 2 | 3 | on: [pull_request_target] 4 | 5 | permissions: 6 | pull-requests: write 7 | contents: write 8 | 9 | jobs: 10 | auto-merge: 11 | if: ${{ github.event.pull_request.user.login == 'dependabot[bot]' && github.repository == 'scala-steward-org/scala-steward-action' }} 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Auto-merge Dependabot PRs 15 | env: 16 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 17 | PR_URL: ${{ github.event.pull_request.html_url }} 18 | run: gh pr merge --auto --squash "$PR_URL" 19 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | ci: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout project 10 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 11 | 12 | - uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0 13 | with: 14 | node-version: '20' 15 | 16 | - name: Run `npm install` 17 | run: npm install 18 | 19 | - name: Run `npm run all` 20 | run: npm run all 21 | 22 | - name: Save PR number 23 | if: ${{ github.event.pull_request.user.login != 'dependabot[bot]' }} 24 | run: echo ${{ github.event.number }} > PR_NUMBER 25 | 26 | - name: Upload artifact with `PR_NUMBER` 27 | if: ${{ github.event.pull_request.user.login != 'dependabot[bot]' }} 28 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 29 | with: 30 | name: PR_NUMBER 31 | path: PR_NUMBER 32 | 33 | - name: Upload artifact with `dist` folder 34 | if: ${{ github.event.pull_request.user.login != 'dependabot[bot]' }} 35 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 36 | with: 37 | name: dist 38 | path: dist/ 39 | 40 | - name: Upload artifact with `covertura.xml` 41 | if: ${{ github.event.pull_request.user.login != 'dependabot[bot]' }} 42 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 43 | with: 44 | name: cobertura 45 | path: coverage/cobertura-coverage.xml 46 | -------------------------------------------------------------------------------- /.github/workflows/code_coverage_comment.yml: -------------------------------------------------------------------------------- 1 | name: Add comment with code-coverage to PR 2 | 3 | on: 4 | workflow_run: 5 | workflows: ['CI'] 6 | branches-ignore: ['dependabot/**'] 7 | types: [completed] 8 | 9 | jobs: 10 | add-comment-with-code-coverage: 11 | runs-on: ubuntu-latest 12 | if: ${{ github.event.pull_request.user.login != 'dependabot[bot]' && github.event.workflow_run.conclusion == 'success' }} 13 | steps: 14 | - name: Checkout project 15 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 16 | 17 | - name: Download cobertura.xml file 18 | uses: dawidd6/action-download-artifact@07ab29fd4a977ae4d2b275087cf67563dfdf0295 # v9 19 | with: 20 | run_id: ${{github.event.workflow_run.id }} 21 | name: cobertura 22 | path: . 23 | 24 | - name: Download PR_NUMBER 25 | uses: dawidd6/action-download-artifact@07ab29fd4a977ae4d2b275087cf67563dfdf0295 # v9 26 | with: 27 | run_id: ${{github.event.workflow_run.id }} 28 | name: PR_NUMBER 29 | path: . 30 | 31 | - name: Extract PR number 32 | id: extract-pr-number 33 | run: | 34 | pr_number=$(cat ./PR_NUMBER) 35 | echo "PR_NUMBER=$pr_number" >> $GITHUB_OUTPUT 36 | 37 | - name: Code Coverage Report 38 | uses: irongut/CodeCoverageSummary@51cc3a756ddcd398d447c044c02cb6aa83fdae95 # v1.3.0 39 | with: 40 | filename: cobertura-coverage.xml 41 | badge: true 42 | format: markdown 43 | output: both 44 | thresholds: '60 80' 45 | 46 | - name: Add Coverage PR Comment 47 | uses: marocchino/sticky-pull-request-comment@52423e01640425a022ef5fd42c6fb5f633a02728 # v2.9.1 48 | with: 49 | recreate: true 50 | header: coverage 51 | number: ${{ steps.extract-pr-number.outputs.PR_NUMBER }} 52 | path: code-coverage-results.md 53 | -------------------------------------------------------------------------------- /.github/workflows/release_auto.yml: -------------------------------------------------------------------------------- 1 | # Releases a new minor version every time a PR is merged into `master`. 2 | # 3 | # It also generates the `dist` folder inside the tag's commit, keeping 4 | # the `master` branch clean. 5 | # 6 | # It will also update the major tag v2 to track the latest tag. 7 | 8 | name: Release new version 9 | 10 | on: 11 | workflow_dispatch: 12 | push: 13 | branches: 14 | - master 15 | 16 | jobs: 17 | new-release: 18 | name: Create new release 19 | runs-on: ubuntu-latest 20 | steps: 21 | - name: Checkout project 22 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 23 | with: 24 | fetch-depth: 0 25 | ref: master 26 | 27 | - uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0 28 | with: 29 | node-version: '20' 30 | 31 | - name: Run `npm install` 32 | run: npm install 33 | 34 | - name: Run `npm run build` 35 | run: npm run build 36 | 37 | - name: Release new version 38 | uses: int128/release-typescript-action@f30250a9bb537e3a541f581122deb035080e8f68 # v1.35.0 39 | with: 40 | major-version: 2 41 | 42 | update-docs: 43 | name: Update documentation 44 | if: ${{ github.actor != 'dependabot[bot]' }} 45 | runs-on: ubuntu-latest 46 | steps: 47 | - name: Checkout project 48 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 49 | with: 50 | fetch-depth: 0 51 | ref: master 52 | 53 | - name: Run `npm install` 54 | run: npm install 55 | 56 | - name: Run `npm run docs` 57 | run: npm run docs 58 | 59 | - name: Commit changes by `npm run docs` 60 | uses: alejandrohdezma/actions/commit-and-push@v1 61 | with: 62 | message: Run `npm run docs` [skip ci] 63 | branch: master 64 | -------------------------------------------------------------------------------- /.github/workflows/release_pr_snapshot.yml: -------------------------------------------------------------------------------- 1 | name: Release a snapshot artifact for a PR 2 | 3 | on: 4 | workflow_run: 5 | workflows: ['CI'] 6 | branches-ignore: ['dependabot/**'] 7 | types: [completed] 8 | 9 | jobs: 10 | release-snapshot-artifact: 11 | runs-on: ubuntu-latest 12 | if: ${{ github.event.pull_request.user.login != 'dependabot[bot]' && github.event.workflow_run.conclusion == 'success' }} 13 | steps: 14 | - name: Download PR_NUMBER 15 | uses: dawidd6/action-download-artifact@07ab29fd4a977ae4d2b275087cf67563dfdf0295 # v9 16 | with: 17 | run_id: ${{github.event.workflow_run.id }} 18 | name: PR_NUMBER 19 | path: . 20 | 21 | - name: Extract PR number 22 | id: extract-pr-number 23 | run: | 24 | pr_number=$(cat ./PR_NUMBER) 25 | echo "PR_NUMBER=$pr_number" >> $GITHUB_OUTPUT 26 | 27 | - name: Checkout project 28 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 29 | 30 | - name: Checkout PR 31 | env: 32 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 33 | run: gh pr checkout ${{ steps.extract-pr-number.outputs.PR_NUMBER }} 34 | 35 | - name: Download dist folder 36 | uses: dawidd6/action-download-artifact@07ab29fd4a977ae4d2b275087cf67563dfdf0295 # v9 37 | with: 38 | run_id: ${{github.event.workflow_run.id }} 39 | name: dist 40 | path: dist 41 | 42 | - name: Remove `dist` from `.gitignore` 43 | run: sed -i -E 's|^/?dist/?||g' .gitignore 44 | 45 | - name: Create snapshot branch 46 | uses: alejandrohdezma/actions/commit-and-push@v1 47 | with: 48 | message: 'Release snapshot for #${{ steps.extract-pr-number.outputs.PR_NUMBER }}' 49 | force-push: 'true' 50 | branch: snapshots/${{ steps.extract-pr-number.outputs.PR_NUMBER }} 51 | 52 | - name: Create Comment 53 | uses: marocchino/sticky-pull-request-comment@52423e01640425a022ef5fd42c6fb5f633a02728 # v2.9.1 54 | with: 55 | recreate: true 56 | header: snapshot 57 | number: ${{ steps.extract-pr-number.outputs.PR_NUMBER }} 58 | message: | 59 | A snapshot release has been created as `snapshots/${{ steps.extract-pr-number.outputs.PR_NUMBER }}`. 60 | 61 | You can test it out with: 62 | 63 | ```yaml 64 | uses: scala-steward-org/scala-steward-action@snapshots/${{ steps.extract-pr-number.outputs.PR_NUMBER }} 65 | ``` 66 | 67 | It will be automatically recreated on any change to this PR. 68 | -------------------------------------------------------------------------------- /.github/workflows/remove_snapshot_branch.yml: -------------------------------------------------------------------------------- 1 | name: Remove snapshot branch 2 | 3 | on: 4 | pull_request_target: 5 | types: 6 | - closed 7 | 8 | jobs: 9 | delete-pr-branch: 10 | runs-on: ubuntu-latest 11 | if: github.event.sender.login != 'dependabot[bot]' 12 | steps: 13 | - name: Checkout project 14 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 15 | 16 | - name: Remove snapshot branch 17 | run: git push origin -d snapshots/${{ github.event.number }} || true 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependency directory 2 | node_modules 3 | 4 | # Rest pulled from https://github.com/github/gitignore/blob/master/Node.gitignore 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # Diagnostic reports (https://nodejs.org/api/report.html) 14 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 15 | 16 | # Runtime data 17 | pids 18 | *.pid 19 | *.seed 20 | *.pid.lock 21 | 22 | # Directory for instrumented libs generated by jscoverage/JSCover 23 | lib-cov 24 | 25 | # Coverage directory used by tools like istanbul 26 | coverage 27 | *.lcov 28 | 29 | # nyc test coverage 30 | .nyc_output 31 | 32 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 33 | .grunt 34 | 35 | # Bower dependency directory (https://bower.io/) 36 | bower_components 37 | 38 | # node-waf configuration 39 | .lock-wscript 40 | 41 | # Compiled binary addons (https://nodejs.org/api/addons.html) 42 | build/Release 43 | 44 | # Dependency directories 45 | jspm_packages/ 46 | 47 | # TypeScript v1 declaration files 48 | typings/ 49 | 50 | # TypeScript cache 51 | *.tsbuildinfo 52 | 53 | # Optional npm cache directory 54 | .npm 55 | 56 | # Optional eslint cache 57 | .eslintcache 58 | 59 | # Optional REPL history 60 | .node_repl_history 61 | 62 | # Output of 'npm pack' 63 | *.tgz 64 | 65 | # Yarn Integrity file 66 | .yarn-integrity 67 | 68 | # dotenv environment variables file 69 | .env 70 | .env.test 71 | 72 | # parcel-bundler cache (https://parceljs.org/) 73 | .cache 74 | 75 | # next.js build output 76 | .next 77 | 78 | # nuxt.js build output 79 | .nuxt 80 | 81 | # vuepress build output 82 | .vuepress/dist 83 | 84 | # Serverless directories 85 | .serverless/ 86 | 87 | # FuseBox cache 88 | .fusebox/ 89 | 90 | # DynamoDB Local files 91 | .dynamodb/ 92 | 93 | # OS metadata 94 | .DS_Store 95 | Thumbs.db 96 | 97 | # Ignore built ts files 98 | __tests__/runner/* 99 | lib/**/* 100 | 101 | # Ignore IntelliJ IDEA files 102 | .idea 103 | 104 | # Ignore Visual Studio Code files 105 | .vscode 106 | 107 | # Ignore `dist` folder, it is generated only inside tag's commits 108 | dist 109 | 110 | # Ignore coverage folder generated by `c8` 111 | coverage 112 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | We are committed to providing a friendly, safe and welcoming environment for 4 | all, regardless of level of experience, gender, gender identity and expression, 5 | sexual orientation, disability, personal appearance, body size, race, ethnicity, 6 | age, religion, nationality, or other such characteristics. 7 | 8 | Everyone is expected to follow the [Scala Code of Conduct](https://www.scala-lang.org/conduct/) 9 | when discussing the project on the available communication channels. If you are being harassed, please 10 | contact us immediately so that we can support you. 11 | 12 | ## Moderation 13 | 14 | For any questions, concerns, or moderation requests please contact a member of the project. 15 | 16 | - [Alejandro Hernández](mailto:info@alejandrohdezma.com) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2020 Alejandro Hernández 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Scala Steward GitHub Action 2 | 3 | A GitHub Action to launch [Scala Steward](https://github.com/scala-steward-org/scala-steward) in your repository. 4 | 5 | 6 | 7 | - [Installation](#installation) 8 | - [Simple workflow using default GitHub Actions App](#simple-workflow-using-default-github-actions-app) 9 | - [Workflow with a custom GitHub App to manage multiple repositories](#workflow-with-a-custom-github-app-to-manage-multiple-repositories) 10 | - [Usage](#usage) 11 | - [Guides](#guides) 12 | - [Contributors](#contributors) 13 | 14 | 15 | 16 | --- 17 | 18 | ## Installation 19 | 20 | You can use this action for [individual repositories](#simple-workflow-using-default-github-actions-app) or you can configure a repository to [manage many repositories](#workflow-with-a-custom-github-app-to-manage-multiple-repositories). 21 | 22 | ### Simple workflow using default GitHub Actions App 23 | 24 | ```yaml 25 | on: 26 | schedule: 27 | - cron: '0 0 * * 0' 28 | 29 | name: Scala Steward 30 | 31 | permissions: 32 | contents: write 33 | pull-requests: write 34 | 35 | jobs: 36 | scala-steward: 37 | runs-on: ubuntu-latest 38 | name: Scala Steward 39 | steps: 40 | - name: Scala Steward 41 | uses: scala-steward-org/scala-steward-action@v2 42 | ``` 43 | 44 | See [Usage](#usage) 45 | 46 | Note that you won't be able to use any of `github-app-*`, `repos-file`, or `github-repository` inputs as the GitHub Actions App can only possibly write to the current repository. 47 | 48 | You will also need to ensure that GitHub Actions has permissions to create and approve pull requests for the [organization](https://docs.github.com/en/organizations/managing-organization-settings/disabling-or-limiting-github-actions-for-your-organization#preventing-github-actions-from-creating-or-approving-pull-requests) or [repository](https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/enabling-features-for-your-repository/managing-github-actions-settings-for-a-repository#preventing-github-actions-from-creating-or-approving-pull-requests). 49 | 50 | 51 | ### Workflow with a custom GitHub App to manage multiple repositories 52 | 53 | To use the Action in your repo, you need to create a GitHub App. Then you need to create a new GitHub Actions workflow file to run this Action. Here is a step-by-step tutorial on how to do it: 54 | 55 |
1. Create a new GitHub App
56 | 57 | If you are creating a GitHub App for your personal account, just click [here][github-app-personal] and it will create one with the base settings already pre-configured. 58 | 59 | On the other hand, if you are creating the App for an organization account, copy [this url][github-app-organization] and replace `my_org` with the name of your organization. 60 | 61 | > You will need to rename the App's name to a handler that is not already taken. You can use `scala-steward-{my-github-login}` if you are creating the app for your personal account; otherwise, you can use `scala-steward-{my-org}`. 62 | 63 | Alternatively, you can follow the official guide for creating a GitHub App :point_down: 64 | 65 |
Official guide
66 | 67 | Follow the GitHub's [Creating a GitHub App](https://docs.github.com/en/developers/apps/building-github-apps/creating-a-github-app) Guide. 68 | 69 | 1. If you're setting up this Action for an organisation-owned repo, note that the step (1) of the "Creating a GitHub App" Guide tells you how to create an organization-level App. 70 | 2. Step (7) of the Guide tells you to specify the homepage – you can write a random URL there. 71 | 3. Step (13) of the Guide tells you to specify the Webhook URL - you don't need it. Uncheck the box. 72 | 4. Step (15) of the Guide asks you which permissions you want your app to have. Specify the following: 73 | - Metadata: Read-only 74 | - Pull requests: Read and write 75 | - Contents: Read and write 76 | 77 |

78 | 79 | Optional: Upload a profile picture for the newly created App. 80 | 81 | 1. Locate the newly created App's Settings. To do so, go to the settings of either your personal profile or of that of your organisation (depending on where you created the App), select "Developer Settings" from the side bar, then click "GitHub Apps". Find your app, and click "Edit" next to it. 82 | - To access your personal settings, click on your profile icon at the top-right corner of the GitHub interface, click "Settings". 83 | - To access the settings of an organisation, click on your profile icon at the top-right, select "Your organizations", find the organisation for which you created an App and click "Settings" for that organisation. 84 | 2. In the settings, locate the "Display information" section and press the "Upload a logo" button. 85 | 3. If you want to use [Scala Steward's official logo](https://github.com/scala-steward-org/scala-steward/raw/main/data/images/scala-steward-logo-circle-0.png) just download it to a folder in your computer and upload it back using the input. Then set "Badge background color" to `#3d5a80` 86 | 87 |
88 | 89 |
2. Install the App
90 | 91 | 1. At the App Settings, at the sidebar, click the "Public page" button, there, click the green "Install" button. 92 | 2. Select whether you'd like to install it account-wide or only for selected repos. If you install it for your entire account (personal or organisation), Scala Steward will try to update every repository in your organization, even if they're not Scala repositories. 93 | 3. Click "Install". 94 | 4. When the new page opens, find its URL and copy the number behind `https://github.com/settings/installations/`. It is the installation ID, you will need it in the following step. 95 | 96 |
97 | 98 |
3. Copy the App ID, the App private key and the installation ID
99 | 100 | Locate the App ID, the installation ID and the App private key for usage in the next step of this tutorial. All of them can be accessed from your App's Settings. 101 | 102 | 1. App ID is available in the "About" section of the Settings. 103 | 2. You should have the installation ID from step (2.4). If you didn't copy it, go to the App's settings and click on "Install App" on the left. On the new page you should see the account where you install the app. Click the "gear" icon on the right. When the next page loads, find its URL and copy the number behind `https://github.com/settings/installations/`. That's the installation ID. 104 | 3. The private key needs to be generated from the "Private keys" section. Clicking the "Generate private key" button will download a `*.pem` file on your computer. Save that file for the following step. 105 | 106 |
107 | 108 |
4. Create repo secrets
109 | 110 | Create repo secrets for the private key, the app id and the installation ID in the repository from where you want to run this action. 111 | 112 | 1. To do so, from the repo's page, click the "Settings" tab. There, select "Secrets" at the sidebar, and click "Actions" at the dropdown menu. Click "New repository secret". 113 | 2. At the "Name" field, enter `APP_PRIVATE_KEY`. Then, open the ".pem" file you downloaded at step (3.3) with a text editor, and copy the contents. Make sure to copy everything, including the first line `-----BEGIN RSA PRIVATE KEY-----` and the last line `-----END RSA PRIVATE KEY-----`. Paste it at the "Value" text area. Click "Add Secret". 114 | 3. Repeat the previous steps (4.1) to add a secret for the app id you recover on step (3.1). Specify `APP_ID` as the name. 115 | 4. Repeat the previous steps (4.1) to add a secret for the installation id you recover on step (3.2). Specify `APP_INSTALLATION_ID` as the name. 116 | 117 |
118 | 119 |
5. Create a new GitHub Actions Workflow
120 | 121 | Create a new GitHub Actions Workflow file, e.g. `.github/workflows/scala-steward.yml`, in the repo where you're installing this Action. Paste the following content into that file: 122 | 123 | ```yaml 124 | on: 125 | schedule: 126 | - cron: '0 0 * * 0' 127 | 128 | name: Launch Scala Steward 129 | 130 | jobs: 131 | scala-steward: 132 | runs-on: ubuntu-latest 133 | name: Launch Scala Steward 134 | steps: 135 | - name: Launch Scala Steward 136 | uses: scala-steward-org/scala-steward-action@v2 137 | with: 138 | github-app-id: ${{ secrets.APP_ID }} 139 | github-app-installation-id: ${{ secrets.APP_INSTALLATION_ID }} 140 | github-app-key: ${{ secrets.APP_PRIVATE_KEY }} 141 | ``` 142 | 143 |
144 | 145 |
6. Scala Steward does its magic :tada:
146 | 147 | If you have used the default cron expression the workflow will launch at 00:00 every Sunday. If you want to change it to a different schedule, you can check [this page](https://crontab.guru). 148 | 149 | When it launches it will send PR to update all the repos selected in step (2.2). 150 | 151 |
152 | 153 | ## Usage 154 | 155 | 156 | ```yaml 157 | - uses: scala-steward-org/scala-steward-action@v2 158 | with: 159 | # Artifact migrations for newer versions of artifacts with 160 | # different group Ids, artifact ids, or both different. 161 | # 162 | # Expects the path to HOCON file with migration/s. 163 | # 164 | # See https://github.com/scala-steward-org/scala-steward/blob/main/docs/artifact-migrations.md 165 | artifact-migrations: '' 166 | 167 | # Author email address to use in commits. If set it will 168 | # override any email retrieved from GitHub. 169 | author-email: '' 170 | 171 | # Author name to use in commits. If set it will override 172 | # any name retrieved from GitHub. 173 | author-name: '' 174 | 175 | # A comma-separated list of branches to update (if not 176 | # provided, the repository's default branch will be 177 | # updated instead). 178 | # 179 | # This option only has effect if updating the current 180 | # repository or using the `github-repository` input. 181 | branches: '' 182 | 183 | # TTL of cache for fetching dependency versions and 184 | # metadata, set it to 0s to disable it. 185 | # 186 | # Default: 2hours 187 | cache-ttl: '' 188 | 189 | # Size of the buffer for the output of an external process 190 | # in lines. 191 | # 192 | # Default: 16384 193 | max-buffer-size: '' 194 | 195 | # Url to download the coursier linux CLI from. 196 | # 197 | # Default: https://github.com/coursier/launchers/raw/master/cs-x86_64-pc-linux.gz 198 | coursier-cli-url: '' 199 | 200 | # The URL of the GitHub API, only use this input if 201 | # you are using GitHub Enterprise. 202 | # 203 | # Default: https://api.github.com 204 | github-api-url: '' 205 | 206 | # If set to `true` the GitHub App information will 207 | # only be used for authentication. 208 | # 209 | # Repositories to update will be read from either 210 | # the `repos-file` or the `github-repository` inputs. 211 | # 212 | # Default: false 213 | github-app-auth-only: '' 214 | 215 | # GitHub App ID. See the "Installation" section of the 216 | # README to learn how to set up the app and how to fill this input. 217 | github-app-id: '' 218 | 219 | # GitHub App Installation ID. See the "Installation" 220 | # section of the README to learn how to set up the app 221 | # and how to fill this input. 222 | github-app-installation-id: '' 223 | 224 | # GitHub App Private Key. See the "Installation" section 225 | # of the README to learn how to set up the app and how to 226 | # fill this input. 227 | github-app-key: '' 228 | 229 | # Repository to update. Will be ignored if either 230 | # `repos-file` is provided or the `github-app-*` 231 | # inputs are and `github-app-auth-only` is not `true`. 232 | # 233 | # Default: ${{ github.repository }} 234 | github-repository: '' 235 | 236 | # GitHub Personal Access Token with permission to create 237 | # branches on repo. 238 | # 239 | # If `github-app-*` inputs are provided an App's 240 | # installation token will be used instead of this one. 241 | # 242 | # Default: ${{ github.token }} 243 | github-token: '' 244 | 245 | # Whether to ignore "opts" files (such as `.jvmopts` 246 | # or `.sbtopts`) when found on repositories or not. 247 | # 248 | # Default: true 249 | ignore-opts-files: '' 250 | 251 | # Mill version to install. Take into account this will 252 | # just affect the global `mill` executable. Scala 253 | # Steward will still respect the version specified in 254 | # your repository while updating it. 255 | # 256 | # Default: 0.12.5 257 | mill-version: '' 258 | 259 | # Other Scala Steward arguments not yet supported by 260 | # this action as a separate argument. 261 | other-args: '' 262 | 263 | # Location of a `.scala-steward.conf` file with default 264 | # values. 265 | # 266 | # If you specify a file and it does not exist in the 267 | # selected branch then this action will fail when it runs. 268 | # 269 | # See https://github.com/scala-steward-org/scala-steward/blob/main/docs/repo-specific-configuration.md 270 | # Default: .github/.scala-steward.conf 271 | repo-config: '' 272 | 273 | # Path to a file containing the list of repositories 274 | # to update in markdown format: 275 | # 276 | # - owner/repo1 277 | # - owner/repo2 278 | # 279 | # This input will be ignored if the `github-app-*` 280 | # inputs are provided and `github-app-auth-only` is 281 | # not `true`. 282 | repos-file: '' 283 | 284 | # Scala Steward version to use. If not provided it 285 | # will use the last one published. 286 | scala-steward-version: '' 287 | 288 | # Scalafix rules for version updates to run after 289 | # certain updates. 290 | # 291 | # Expects the path to HOCON file with migration/s. 292 | # 293 | # See https://github.com/scala-steward-org/scala-steward/blob/main/docs/scalafix-migrations.md 294 | scalafix-migrations: '' 295 | 296 | # Whether to sign commits or not. 297 | # 298 | # Default: false 299 | sign-commits: '' 300 | 301 | # Key ID of GPG key to use for signing commits. See the 302 | # "Signing commits with GPG" section to learn how to 303 | # prepare the environment and fill this input. 304 | signing-key: '' 305 | 306 | # Timeout for external process invocations. 307 | # 308 | # Default: 20min 309 | timeout: '' 310 | ``` 311 | 312 | 313 | ## Guides 314 | 315 |
Manually triggering a run
316 | 317 | You can manually trigger workflow runs using the [workflow_dispatch](https://docs.github.com/en/free-pro-team@latest/actions/reference/events-that-trigger-workflows#workflow_dispatch) event: 318 | 319 | ```diff 320 | on: 321 | + workflow_dispatch: 322 | schedule: 323 | - cron: '0 0 * * 0' 324 | 325 | name: Launch Scala Steward 326 | 327 | jobs: 328 | scala-steward: 329 | runs-on: ubuntu-latest 330 | name: Launch Scala Steward 331 | steps: 332 | - name: Launch Scala Steward 333 | uses: scala-steward-org/scala-steward-action@v2 334 | with: 335 | github-app-id: ${{ secrets.APP_ID }} 336 | github-app-installation-id: ${{ secrets.APP_INSTALLATION_ID }} 337 | github-app-key: ${{ secrets.APP_PRIVATE_KEY }} 338 | ``` 339 | Once you've added this trigger GitHub will show a "Run workflow" button at the workflow page. 340 | 341 |
342 |
343 | 344 |
Specify JVM version
345 | 346 | If you would like to specify a specific Java version (e.g Java 11) please add the following step before `Launch Scala Steward` step: 347 | 348 | ```yaml 349 | - name: Set up JDK 11 350 | uses: actions/setup-java@v3 351 | with: 352 | java-version: 11 353 | distribution: temurin 354 | ``` 355 | 356 |
357 |
358 | 359 |
Add JVM options to the running JVM
360 | 361 | If you would like to add JVM options (such as `-Xmx`) to the Scala Steward JVM process please add the following to the main `Launch Scala Steward` step: 362 | 363 | ```yaml 364 | - name: Launch Scala Steward 365 | uses: scala-steward-org/scala-steward-action@v2 366 | env: 367 | JAVA_OPTS: "-XX:+UseG1GC -Xms4G -Xmx4G -Xss2M -XX:MetaspaceSize=512M" 368 | ``` 369 | 370 |
371 |
372 | 373 |
Updating a specific repository
374 | 375 | When using the `github-app-*` inputs, Scala Steward will always retrieve the list of repositories to update from the App's installation. You can override this by setting `github-app-auth-only` to `'true'`. This way the action will only use the app credentials to authenticate and will update the repository set on the `github-repository` input (defaults to the current repository). 376 | 377 | ```yaml 378 | on: 379 | schedule: 380 | - cron: '0 0 * * 0' 381 | 382 | name: Launch Scala Steward 383 | 384 | jobs: 385 | scala-steward: 386 | runs-on: ubuntu-latest 387 | name: Launch Scala Steward 388 | steps: 389 | - name: Launch Scala Steward 390 | uses: scala-steward-org/scala-steward-action@v2 391 | with: 392 | github-app-id: ${{ secrets.APP_ID }} 393 | github-app-installation-id: ${{ secrets.APP_INSTALLATION_ID }} 394 | github-app-key: ${{ secrets.APP_PRIVATE_KEY }} 395 | github-app-auth-only: 'true' 396 | ``` 397 | 398 | To update a repository other than the one where the Action runs, we can override the `github-repository` input. Just set it to the name (owner/repo) of the repository you would like to update. 399 | 400 | ```yaml 401 | - name: Launch Scala Steward 402 | uses: scala-steward-org/scala-steward-action@v2 403 | with: 404 | github-app-id: ${{ secrets.APP_ID }} 405 | github-app-installation-id: ${{ secrets.APP_INSTALLATION_ID }} 406 | github-app-key: ${{ secrets.APP_PRIVATE_KEY }} 407 | github-app-auth-only: 'true' 408 | github-repository: owner/repo 409 | ``` 410 | 411 |
412 |
413 | 414 |
Update specific repositories (listed on a file)
415 | 416 | When using the `github-app-*` inputs, Scala Steward will always retrieve the list of repositories to update from the App's installation. You can override this by setting `github-app-auth-only` to `'true'`. This way the action will only use the app credentials to authenticate and will allow other mechanisms for selecting which repository should be updated. For example, you can specify a list of repositories in a markdown file. 417 | 418 | 1. Create a file containing the list of repositories in markdown format: 419 | ```markdown 420 | # repos.md 421 | - owner/repo_1 422 | - owner/repo_2 423 | ``` 424 | 2. Put that file inside the repository directory (so it is accessible to Scala Steward's action). 425 | 3. Provide it to the action using `repos-file`: 426 | ```yaml 427 | - name: Checkout repository so `repos.md` is available 428 | uses: actions/checkout@v2 429 | 430 | - name: Launch Scala Steward 431 | uses: scala-steward-org/scala-steward-action@v2 432 | with: 433 | github-app-id: ${{ secrets.APP_ID }} 434 | github-app-installation-id: ${{ secrets.APP_INSTALLATION_ID }} 435 | github-app-key: ${{ secrets.APP_PRIVATE_KEY }} 436 | github-app-auth-only: 'true' 437 | repos-file: 'repos.md' 438 | ``` 439 | 440 | > 441 | 442 | > This input (if present) will always take precedence over `github-repository`. 443 | 444 |
445 |
446 | 447 |
Update one or more specific branches
448 | 449 | > **Important!** This input is only used when using the `github-repository` input (see the "Updating a specific repository" guide). For cases where the `repos-file` input is used (see the "Update specific repositories (listed on a file)" guide), you should follow the [instructions to update multiple branches in a repository](https://github.com/scala-steward-org/scala-steward/blob/main/docs/faq.md#can-scala-steward-update-multiple-branches-in-a-repository). 450 | 451 | > **This input won't have any effect when using a GitHub App for listing the repositories to update.** 452 | 453 | By default, Scala Steward uses the repository's default branch to make the updates. If you want to customize that behavior, you can use the `branches` input: 454 | 455 | ```yml 456 | - name: Launch Scala Steward 457 | uses: scala-steward-org/scala-steward-action@v2 458 | with: 459 | github-app-id: ${{ secrets.APP_ID }} 460 | github-app-installation-id: ${{ secrets.APP_INSTALLATION_ID }} 461 | github-app-key: ${{ secrets.APP_PRIVATE_KEY }} 462 | github-app-auth-only: 'true' 463 | github-repository: owner/repo 464 | branches: main,0.1.x,0.2.x 465 | ``` 466 | 467 |
468 |
469 | 470 |
Disable ignoring OPTS files
471 | 472 | By default, Scala Steward will ignore "opts" files (such as `.jvmopts` or `.sbtopts`) when found on repositories, if you want to disable this feature, use the `ignore-opts-files` input: 473 | 474 | ```yaml 475 | - name: Launch Scala Steward 476 | uses: scala-steward-org/scala-steward-action@v2 477 | with: 478 | github-app-id: ${{ secrets.APP_ID }} 479 | github-app-installation-id: ${{ secrets.APP_INSTALLATION_ID }} 480 | github-app-key: ${{ secrets.APP_PRIVATE_KEY }} 481 | ignore-opts-files: false 482 | ``` 483 | 484 |
485 |
486 | 487 |
Run Scala Steward with step debug logging
488 | 489 | You just need to enable [GitHub Actions' "step debug logging"](https://docs.github.com/en/actions/monitoring-and-troubleshooting-workflows/enabling-debug-logging#enabling-step-debug-logging) and Scala Steward will start automatically in debug mode too. 490 | 491 | For this you must set the following secret in the repository that contains the workflow: `ACTIONS_STEP_DEBUG` to `true` (as stated in GitHub's documentation). 492 | 493 | > Alternatively, if you are re-running a failed job and want to re-run it in debug 494 | > mode, follow this tutorial and check `Enable debug logging` before clicking on 495 | > `Re-run jobs`. 496 | > 497 | > ![](https://docs.github.com/assets/cb-11530/images/help/repository/enable-debug-logging.png) 498 | 499 |
500 |
501 | 502 |
Running locally to attach a JVM debugger
503 | 504 | When debugging the behaviour of Scala Steward, it can be helpful to run Scala Steward 505 | locally, while mimicking the settings used by the Scala Steward GitHub Action, so that 506 | a debugger can be attached - [the Guardian have notes on how they do that](https://github.com/guardian/scala-steward-public-repos/blob/main/running-locally.md), 507 | which may provide a helpful example if you need to do that in your own organisation. 508 | 509 |
510 |
511 | 512 |
Using a Personal Access Token (instead of the GitHub App)
513 | 514 | If for any reason you want to use the default GitHub Token available in GitHub Actions, you won't be able to use the `github-app-*` inputs. If you still want to use it you just need to remove all the `github-app-*` inputs and follow these steps: 515 | 516 | 1. Generate a [GitHub Personal Access Token](https://github.com/settings/tokens) with `repo` permissions for reading/writing in the repository/repositories you wish to update. 517 | 2. Add it as a repository secret. 518 | 3. Follow either the `Updating a specific repository` or the `Update specific repositories (listed on a file)` guides to provide a repository to update. 519 | 3. Provide the token to the action using the `github-token` input. 520 | 521 | **Example updating the current repository with the default GitHub Token** 522 | 523 | ```yaml 524 | - name: Launch Scala Steward 525 | uses: scala-steward-org/scala-steward-action@v2 526 | with: 527 | github-token: ${{ secrets.REPO_GITHUB_TOKEN }} 528 | ``` 529 | 530 | **Example updating a specific repository with the default GitHub Token** 531 | 532 | ```yaml 533 | - name: Launch Scala Steward 534 | uses: scala-steward-org/scala-steward-action@v2 535 | with: 536 | github-repository: owner/repo 537 | github-token: ${{ secrets.REPO_GITHUB_TOKEN }} 538 | ``` 539 | 540 | **Example updating a list of repositories (from a file) with the default GitHub Token** 541 | 542 | ```yaml 543 | - name: Launch Scala Steward 544 | uses: scala-steward-org/scala-steward-action@v2 545 | with: 546 | github-repository: owner/repo 547 | github-token: ${{ secrets.REPO_GITHUB_TOKEN }} 548 | repos-file: 'repos.md' 549 | ``` 550 | 551 | Beware that using the Personal Access Token will make it look like it's you who submitted all the PRs. The workaround for this is to create a separate GitHub account for the Action and give it the [Collaborator](https://help.github.com/en/github/setting-up-and-managing-your-github-user-account/inviting-collaborators-to-a-personal-repository) permission in the repository or repositories you wish to update. 552 | 553 | Make sure the account you choose has *Name* and *Public email* fields defined in its [Public Profile](https://github.com/settings/profile), as they will be used by Scala Steward to make commits. 554 | 555 | If the account has [personal email address protection](https://help.github.com/en/github/setting-up-and-managing-your-github-user-account/blocking-command-line-pushes-that-expose-your-personal-email-address) enabled, then you will need to explicitly specify an email to use in commits: 556 | 557 | ```yaml 558 | - name: Launch Scala Steward 559 | uses: scala-steward-org/scala-steward-action@v2 560 | with: 561 | github-token: ${{ secrets.REPO_GITHUB_TOKEN }} 562 | author-email: 12345+octocat@users.noreply.github.com 563 | ``` 564 | 565 |
566 |
567 | 568 |
Sign commits created by Scala Steward
569 | 570 | > Signing commits only take place when using a GitHub Personal Access Token (see the "Using a Personal Access Token (instead of the GitHub App)" guide). 571 | 572 | If you want commits created by Scala Steward to be automatically signed with a GPG key, follow these steps: 573 | 574 | 1. Generate a new GPG key following [GitHub's own tutorial](https://help.github.com/en/github/authenticating-to-github/generating-a-new-gpg-key). 575 | 2. Add your new GPG key to your [user's GitHub account](https://github.com/settings/keys) following [GitHub's own tutorial](https://help.github.com/en/github/authenticating-to-github/adding-a-new-gpg-key-to-your-github-account). 576 | 3. Export the GPG private key as an ASCII armored version to your clipboard (change `joe@foo.bar` with your key email address): 577 | 578 | ```bash 579 | # macOS 580 | gpg --armor --export-secret-key joe@foo.bar | pbcopy 581 | 582 | # Ubuntu (assuming GNU base64) 583 | gpg --armor --export-secret-key joe@foo.bar -w0 | xclip 584 | 585 | # Arch 586 | gpg --armor --export-secret-key joe@foo.bar | sed -z 's;\n;;g' | xclip -selection clipboard -i 587 | 588 | # FreeBSD (assuming BSD base64) 589 | gpg --armor --export-secret-key joe@foo.bar | xclip 590 | ``` 591 | 592 | 4. Paste your clipboard as a new `GPG_PRIVATE_KEY` repository secret. 593 | 5. If the key is passphrase protected, add the passphrase as another repository secret called `GPG_PASSPHRASE`. 594 | 6. Import it to the workflow using an action such us [crazy-max/ghaction-import-gpg](https://github.com/crazy-max/ghaction-import-gpg): 595 | 596 | ```yaml 597 | - name: Import GPG key 598 | uses: crazy-max/ghaction-import-gpg@v2 599 | with: 600 | git_user_signingkey: true 601 | env: 602 | GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} 603 | PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} 604 | ``` 605 | 606 | 7. Obtain the key ID for the key that should be used. For instance, in the following example, the GPG key ID is `3AA5C34371567BD2`: 607 | 608 | ``` 609 | $ gpg --list-secret-keys --keyid-format=long 610 | 611 | ~/.gnupg/secring.gpg 612 | ------------------------------------ 613 | sec 4096R/3AA5C34371567BD2 2022-01-01 614 | uid My Name 615 | ssb 4096R/42B317FD4BA89E7A 2022-01-01 616 | ``` 617 | 618 | 8. Copy the key ID and paste it as the content of a new repository secret, named `GPG_SIGNING_KEY_ID`. 619 | 620 | 9. Tell Scala Steward to sign commits using the `sign-commits` input. Use as well the `signing-key` parameter to allow Scala Steward to use the correct key: 621 | 622 | ```yaml 623 | - name: Launch Scala Steward 624 | uses: scala-steward-org/scala-steward-action@v2 625 | with: 626 | github-token: ${{ secrets.REPO_GITHUB_TOKEN }} 627 | signing-key: ${{ secrets.GPG_SIGNING_KEY_ID }} 628 | sign-commits: true 629 | ``` 630 | 631 | 10. **Optional**. By default, Scala Steward will use the email/name of the user that created the token added in `github-token`, if you want to override that behavior, you can use `author-email`/`author-name` inputs, for example with the values extracted from the imported private key: 632 | 633 | ```yaml 634 | - name: Import GPG key 635 | id: import_gpg 636 | uses: crazy-max/ghaction-import-gpg@v2 637 | with: 638 | git_user_signingkey: true 639 | env: 640 | GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} 641 | PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} 642 | 643 | - name: Launch Scala Steward 644 | uses: scala-steward-org/scala-steward-action@v2 645 | with: 646 | github-token: ${{ secrets.REPO_GITHUB_TOKEN }} 647 | signing-key: ${{ secrets.GPG_SIGNING_KEY_ID }} 648 | sign-commits: true 649 | author-email: ${{ steps.import_gpg.outputs.email }} 650 | author-name: ${{ steps.import_gpg.outputs.name }} 651 | ``` 652 | 653 |
654 |
655 | 656 | ## Contributors 657 | 658 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): 659 | 660 | 661 | 662 | 663 | 664 | 665 | 666 | 667 | 668 | 669 | 670 | 671 | 672 | 673 | 674 | 675 | 676 | 677 | 678 | 679 | 680 | 681 | 682 | 683 | 684 | 685 | 686 | 687 | 688 | 689 | 690 | 691 | 692 | 693 | 694 | 695 | 696 | 697 | 698 | 699 | 700 | 701 | 702 | 703 | 704 | 705 | 706 | 707 | 708 | 709 | 710 |
Adam Warski
Adam Warski

💬
Alejandro Hernández
Alejandro Hernández

💻
Alexey Alekhin
Alexey Alekhin

💻
Ali Salim Rashid
Ali Salim Rashid

💻
Anatolii Kmetiuk
Anatolii Kmetiuk

📖
Antonio Gelameris
Antonio Gelameris

💻
Arman Bilge
Arman Bilge

🐛 💻
Chris Kipp
Chris Kipp

🐛 💻
Elias Court
Elias Court

💻
Ewout ter Hoeven
Ewout ter Hoeven

💻
Filipe Regadas
Filipe Regadas

📖
Florian Meriaux
Florian Meriaux

🐛
Francis De Brabandere
Francis De Brabandere

🐛
Frank Thomas
Frank Thomas

💻
Jamie Shiell
Jamie Shiell

🐛
Jeff Boutotte
Jeff Boutotte

💻
Jyrki Hokkanen
Jyrki Hokkanen

🐛
Leo Benkel
Leo Benkel

🐛
Marcelo Carlos
Marcelo Carlos

🐛
Matthew Tovbin
Matthew Tovbin

💻
Michele Pinto
Michele Pinto

🤔
Milan van der Meer
Milan van der Meer

🐛
Pavel Boldyrev
Pavel Boldyrev

💻
Spencer Perkins
Spencer Perkins

📖
Stefanos Pliakos
Stefanos Pliakos

🤔
TATSUNO Yasuhiro
TATSUNO Yasuhiro

💻
Takumi Kadowaki
Takumi Kadowaki

💻
Victor Sollerhed
Victor Sollerhed

💻
Yannick Heiber
Yannick Heiber

💻 🐛
kenji yoshida
kenji yoshida

💻 💬
ryota0624
ryota0624

💻
yokra
yokra

📖
711 | 712 | 713 | 714 | 715 | 716 | 717 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! 718 | 719 | [github-app-personal]: https://github.com/settings/apps/new?name=scala-steward&description=GitHub%20App%20to%20facilitate%20running%20Scala%20Steward%20against%20my%20repositories&url=https://github.com/scala-steward-org/scala-steward&public=false&webhook_active=false&pull_requests=write&contents=write 720 | [github-app-organization]: https://github.com/organizations/my_org/settings/apps/new?name=scala-steward&description=GitHub%20App%20to%20facilitate%20running%20Scala%20Steward%20against%20my%20repositories&url=https://github.com/scala-steward-org/scala-steward&public=false&webhook_active=false&pull_requests=write&contents=write 721 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: Scala Steward GitHub Action 2 | author: The Scala Steward contributors 3 | description: 🤖 A bot that helps you keeping your Scala projects up-to-date 4 | branding: 5 | icon: arrow-up-circle 6 | color: red 7 | 8 | inputs: 9 | artifact-migrations: 10 | description: | 11 | Artifact migrations for newer versions of artifacts with 12 | different group Ids, artifact ids, or both different. 13 | 14 | Expects the path to HOCON file with migration/s. 15 | 16 | See https://github.com/scala-steward-org/scala-steward/blob/main/docs/artifact-migrations.md 17 | required: false 18 | author-email: 19 | description: | 20 | Author email address to use in commits. If set it will 21 | override any email retrieved from GitHub. 22 | required: false 23 | author-name: 24 | description: | 25 | Author name to use in commits. If set it will override 26 | any name retrieved from GitHub. 27 | required: false 28 | branches: 29 | description: | 30 | A comma-separated list of branches to update (if not 31 | provided, the repository's default branch will be 32 | updated instead). 33 | 34 | This option only has effect if updating the current 35 | repository or using the `github-repository` input. 36 | required: false 37 | cache-ttl: 38 | description: | 39 | TTL of cache for fetching dependency versions and 40 | metadata, set it to 0s to disable it. 41 | default: 2hours 42 | required: false 43 | max-buffer-size: 44 | description: | 45 | Size of the buffer for the output of an external process 46 | in lines. The default is 16384. 47 | default: "16384" 48 | required: false 49 | coursier-cli-url: 50 | description: | 51 | Url to download the coursier linux CLI from. 52 | default: https://github.com/coursier/launchers/raw/master/cs-x86_64-pc-linux.gz 53 | required: false 54 | extra-jars: 55 | description: | 56 | Extra JARs to be added to the classpath of the 57 | launched application. Directories accepted too. 58 | required: false 59 | github-api-url: 60 | description: | 61 | The URL of the GitHub API, only use this input if 62 | you are using GitHub Enterprise. 63 | default: ${{ github.api_url }} 64 | required: false 65 | github-app-auth-only: 66 | description: | 67 | If set to `true` the GitHub App information will 68 | only be used for authentication. 69 | 70 | Repositories to update will be read from either 71 | the `repos-file` or the `github-repository` inputs. 72 | default: "false" 73 | required: false 74 | github-app-id: 75 | description: | 76 | GitHub App ID. See the "Installation" section of the 77 | README to learn how to set up the app and how to fill this input. 78 | required: false 79 | github-app-installation-id: 80 | description: | 81 | GitHub App Installation ID. See the "Installation" 82 | section of the README to learn how to set up the app 83 | and how to fill this input. 84 | required: false 85 | github-app-key: 86 | description: | 87 | GitHub App Private Key. See the "Installation" section 88 | of the README to learn how to set up the app and how to 89 | fill this input. 90 | required: false 91 | github-repository: 92 | description: | 93 | Repository to update. Will be ignored if either 94 | `repos-file` is provided or the `github-app-*` 95 | inputs are and `github-app-auth-only` is not `true`. 96 | required: false 97 | default: ${{ github.repository }} 98 | github-token: 99 | description: | 100 | GitHub Personal Access Token with permission to create 101 | branches on repo. 102 | 103 | If `github-app-*` inputs are provided an App's 104 | installation token will be used instead of this one. 105 | required: false 106 | default: ${{ github.token }} 107 | ignore-opts-files: 108 | description: | 109 | Whether to ignore "opts" files (such as `.jvmopts` 110 | or `.sbtopts`) when found on repositories or not. 111 | default: "true" 112 | required: false 113 | mill-version: 114 | description: | 115 | Mill version to install. Take into account this will 116 | just affect the global `mill` executable. Scala 117 | Steward will still respect the version specified in 118 | your repository while updating it. 119 | default: "0.12.5" 120 | required: false 121 | other-args: 122 | description: | 123 | Other Scala Steward arguments not yet supported by 124 | this action as a separate argument. 125 | required: false 126 | repo-config: 127 | description: | 128 | Location of a `.scala-steward.conf` file with default 129 | values. 130 | 131 | If you specify a file and it does not exist in the 132 | selected branch then this action will fail when it runs. 133 | 134 | See https://github.com/scala-steward-org/scala-steward/blob/main/docs/repo-specific-configuration.md 135 | default: ".github/.scala-steward.conf" 136 | required: false 137 | repos-file: 138 | description: | 139 | Path to a file containing the list of repositories 140 | to update in markdown format: 141 | 142 | - owner/repo1 143 | - owner/repo2 144 | 145 | This input will be ignored if the `github-app-*` 146 | inputs are provided and `github-app-auth-only` is 147 | not `true`. 148 | required: false 149 | scala-steward-version: 150 | description: | 151 | Scala Steward version to use. If not provided it 152 | will use the last one published. 153 | required: false 154 | scalafix-migrations: 155 | description: | 156 | Scalafix rules for version updates to run after 157 | certain updates. 158 | 159 | Expects the path to HOCON file with migration/s. 160 | 161 | See https://github.com/scala-steward-org/scala-steward/blob/main/docs/scalafix-migrations.md 162 | required: false 163 | sign-commits: 164 | description: | 165 | Whether to sign commits or not. 166 | default: "false" 167 | required: false 168 | signing-key: 169 | description: | 170 | Key ID of GPG key to use for signing commits. See the 171 | "Signing commits with GPG" section to learn how to 172 | prepare the environment and fill this input. 173 | default: "" 174 | required: false 175 | timeout: 176 | description: | 177 | Timeout for external process invocations. 178 | default: 20min 179 | required: false 180 | 181 | runs: 182 | using: node20 183 | pre: dist/pre.js 184 | main: dist/main.js 185 | post: dist/post.js 186 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "scala-steward-action", 3 | "description": "A GitHub Action to launch Scala Steward in your repository", 4 | "type": "module", 5 | "scripts": { 6 | "build": "npm run build-pre && npm run build-main && npm run build-post", 7 | "build-pre": "ncc build --target es2022 src/action/pre.ts && mv dist/index.js dist/pre.js", 8 | "build-main": "ncc build --target es2022 src/action/main.ts && mv dist/index.js dist/main.js", 9 | "build-post": "ncc build --target es2022 src/action/post.ts && mv dist/index.js dist/post.js", 10 | "docs": "node --loader ts-node/esm src/utils/docs.ts && markdown-toc-gen update README.md", 11 | "contributors:add": "all-contributors add", 12 | "contributors:generate": "all-contributors generate", 13 | "contributors:check": "all-contributors check", 14 | "test": "xo && NODE_OPTIONS='--loader=ts-node/esm --experimental-specifier-resolution=node' c8 ava", 15 | "report": "c8 report", 16 | "all": "npm run build && npm test && npm run report" 17 | }, 18 | "engines": { 19 | "node": ">=20" 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "git+https://github.com/scala-steward-org/scala-steward-action.git" 24 | }, 25 | "keywords": [ 26 | "actions", 27 | "scala", 28 | "dependency-update", 29 | "scala-steward" 30 | ], 31 | "author": "alejandrohdezma", 32 | "license": "Apache-2.0", 33 | "dependencies": { 34 | "@actions/cache": "^4.0.0", 35 | "@actions/core": "^1.11.1", 36 | "@actions/exec": "^1.1.1", 37 | "@actions/github": "^6.0.0", 38 | "@actions/io": "^1.1.3", 39 | "@actions/tool-cache": "^2.0.2", 40 | "@octokit/auth-app": "^7.1.4", 41 | "@octokit/request": "^9.1.3", 42 | "@types/node-fetch": "^2.6.11", 43 | "@types/sinon": "^17.0.3", 44 | "jssha": "^3.3.1", 45 | "node-fetch": "^2.7.0" 46 | }, 47 | "devDependencies": { 48 | "@ava/typescript": "^5.0.0", 49 | "@tsconfig/node20": "^20.1.5", 50 | "@types/js-yaml": "^4.0.9", 51 | "@types/node": "^22.13.14", 52 | "@vercel/ncc": "^0.38.3", 53 | "all-contributors-cli": "^6.26.1", 54 | "ava": "^6.2.0", 55 | "c8": "^10.1.3", 56 | "js-yaml": "^4.1.0", 57 | "markdown-toc-gen": "^1.2.0", 58 | "sinon": "^20.0.0", 59 | "ts-node": "^10.9.2", 60 | "ts-pattern": "^5.7.0", 61 | "typescript": "^5.8.2", 62 | "xo": "^0.60.0" 63 | }, 64 | "xo": { 65 | "space": true, 66 | "semicolon": false, 67 | "rules": { 68 | "new-cap": 0, 69 | "ava/no-ignored-test-files": 0, 70 | "n/file-extension-in-import": 0, 71 | "node/prefer-global/process": 0, 72 | "import/extensions": 0, 73 | "@typescript-eslint/naming-convention": 0, 74 | "@typescript-eslint/no-empty-function": 0, 75 | "unicorn/prefer-node-protocol": 0 76 | } 77 | }, 78 | "ava": { 79 | "files": [ 80 | "src/**/*.test.ts" 81 | ], 82 | "extensions": { 83 | "ts": "module" 84 | } 85 | }, 86 | "c8": { 87 | "all": true, 88 | "src": "src", 89 | "exclude": [ 90 | "src/core/!(types.ts)", 91 | "src/action", 92 | "src/**/*.test.ts", 93 | "src/utils" 94 | ], 95 | "reporter": [ 96 | "text", 97 | "cobertura" 98 | ] 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/action/main.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import os from 'os' 3 | import process from 'process' 4 | import * as cache from '@actions/cache' 5 | import * as core from '@actions/core' 6 | import {getOctokit} from '@actions/github' 7 | import * as io from '@actions/io' 8 | import {createAppAuth} from '@octokit/auth-app' 9 | import {request} from '@octokit/request' 10 | import {type Files} from '../core/files' 11 | import {type Logger} from '../core/logger' 12 | import {nonEmpty, NonEmptyString} from '../core/types' 13 | import * as coursier from '../modules/coursier' 14 | import {GitHub} from '../modules/github' 15 | import {Input, type GitHubAppInfo} from '../modules/input' 16 | import {Workspace} from '../modules/workspace' 17 | 18 | /** 19 | * Runs the action main code. In order it will do the following: 20 | * - Recover user inputs 21 | * - Get authenticated user data from provided GitHub Token 22 | * - Prepare Scala Steward's workspace 23 | * - Run Scala Steward using Coursier. 24 | */ 25 | async function run(): Promise { 26 | try { 27 | const logger: Logger = core 28 | const files: Files = {...fs, ...io} 29 | const inputs = Input.from(core, files, logger).all() 30 | const gitHubApiUrl = inputs.github.apiUrl.value 31 | const gitHubToken = async () => await gitHubAppToken(inputs.github.app, gitHubApiUrl, 'installation') ?? inputs.github.token.value 32 | const octokit = getOctokit(await gitHubToken(), {baseUrl: gitHubApiUrl}) 33 | const github = GitHub.from(logger, octokit) 34 | const workspace = Workspace.from(logger, files, os, cache) 35 | 36 | const user = await gitHubAppToken(inputs.github.app, gitHubApiUrl, 'app') 37 | .then(appToken => appToken ? getOctokit(appToken, {baseUrl: gitHubApiUrl}) : undefined) 38 | .then(async octokit => octokit ? octokit.rest.apps.getAuthenticated() : undefined) 39 | .then(async response => response ? github.getAppUser(response.data.slug) : github.getAuthUser()) 40 | 41 | await workspace.prepare(inputs.steward.repos, gitHubToken, inputs.github.app) 42 | await workspace.restoreWorkspaceCache() 43 | 44 | if (process.env.RUNNER_DEBUG) { 45 | core.debug('🐛 Debug mode activated for Scala Steward') 46 | core.exportVariable('LOG_LEVEL', 'TRACE') 47 | core.exportVariable('ROOT_LOG_LEVEL', 'TRACE') 48 | } 49 | 50 | const app = inputs.steward.version 51 | ? `org.scala-steward:scala-steward-core_2.13:${inputs.steward.version.value}` 52 | : 'scala-steward' 53 | 54 | try { 55 | await coursier.launch(app, [ 56 | argument('--workspace', workspace.workspace), 57 | argument('--repos-file', workspace.repos_md), 58 | argument('--git-ask-pass', workspace.askpass_sh), 59 | argument('--git-author-email', inputs.commits.author.email ?? user.email()), 60 | argument('--git-author-name', inputs.commits.author.name ?? user.name()), 61 | argument('--forge-login', user.login()), 62 | argument('--env-var', nonEmpty('"SBT_OPTS=-Xmx2048m -Xss8m -XX:MaxMetaspaceSize=512m"')), 63 | argument('--process-timeout', inputs.steward.timeout), 64 | argument('--forge-api-host', inputs.github.apiUrl), 65 | argument('--ignore-opts-files', inputs.steward.ignoreOptsFiles), 66 | argument('--sign-commits', inputs.commits.sign.enabled), 67 | argument('--git-author-signing-key', inputs.commits.sign.key), 68 | argument('--cache-ttl', inputs.steward.cacheTtl), 69 | argument('--max-buffer-size', inputs.steward.maxBufferSize), 70 | argument('--scalafix-migrations', inputs.migrations.scalafix), 71 | argument('--artifact-migrations', inputs.migrations.artifacts), 72 | argument('--repo-config', inputs.steward.defaultConfiguration), 73 | argument('--github-app-id', inputs.github.app && !inputs.github.app.authOnly ? inputs.github.app.id : undefined), 74 | argument('--github-app-key-file', inputs.github.app && !inputs.github.app.authOnly ? workspace.app_pem : undefined), 75 | '--do-not-fork', 76 | '--disable-sandbox', 77 | inputs.steward.extraArgs?.value.split(' ') ?? [], 78 | ], inputs.steward.extraJars) 79 | 80 | if (files.existsSync(workspace.runSummary_md)) { 81 | logger.info(`✓ Run Summary file: ${workspace.runSummary_md}`) 82 | 83 | const summaryMarkdown = files.readFileSync(workspace.runSummary_md, 'utf8') 84 | await core.summary.addRaw(summaryMarkdown).write() 85 | } 86 | } finally { 87 | await workspace.purgeTempFilesAndSaveCache() 88 | await workspace.cancelTokenRefresh() 89 | } 90 | } catch (error: unknown) { 91 | core.setFailed(` ✕ ${(error as Error).message}`) 92 | } 93 | } 94 | 95 | /** 96 | * Returns a GitHub App Token. 97 | * 98 | * @param app The GitHub App information. 99 | * @param gitHubApiUrl The GitHub API URL. 100 | * @param type The type of token to retrieve, either `app` or `installation`. 101 | * @returns the GitHub App Token for the provided installation. 102 | */ 103 | async function gitHubAppToken(app: GitHubAppInfo | undefined, gitHubApiUrl: string, type: 'app' | 'installation') { 104 | if (!app) { 105 | return undefined 106 | } 107 | 108 | const auth = createAppAuth({ 109 | appId: app.id.value, 110 | privateKey: app.key.value, 111 | request: request.defaults({ 112 | baseUrl: gitHubApiUrl, 113 | }), 114 | }) 115 | 116 | const response = type === 'app' 117 | ? await auth({type: 'app'}) 118 | : (app.installation ? await auth({type: 'installation', installationId: app.installation.value, refresh: true}) : undefined) 119 | 120 | return response?.token 121 | } 122 | 123 | /** 124 | * Creates an optional argument depending on an input's value. 125 | * 126 | * @param name Name of the arg being added. 127 | * @param value The argument's value, empty string, false booleans or undefined will be skipped. 128 | * @returns the argument to add if it should be added; otherwise returns `[]`. 129 | */ 130 | function argument(name: string, value: NonEmptyString | boolean | undefined) { 131 | if (value instanceof NonEmptyString) { 132 | return [name, value.value] 133 | } 134 | 135 | if (value === undefined) { 136 | return [] 137 | } 138 | 139 | return value ? [name] : [] 140 | } 141 | 142 | // eslint-disable-next-line unicorn/prefer-top-level-await 143 | void run() 144 | -------------------------------------------------------------------------------- /src/action/post.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import os from 'os' 3 | import * as core from '@actions/core' 4 | import * as io from '@actions/io' 5 | import * as cache from '@actions/cache' 6 | import * as coursier from '../modules/coursier' 7 | import * as mill from '../modules/mill' 8 | import {type Logger} from '../core/logger' 9 | import {Workspace} from '../modules/workspace' 10 | import {type Files} from '../core/files' 11 | 12 | /** 13 | * Performs a cleanup of all the artifacts/folders created by this action. 14 | */ 15 | async function run(): Promise { 16 | try { 17 | const logger: Logger = core 18 | const files: Files = {...fs, ...io} 19 | const workspace = Workspace.from(logger, files, os, cache) 20 | 21 | await workspace.remove() 22 | core.info('🗑 Scala Steward\'s workspace removed') 23 | 24 | await coursier.remove() 25 | core.info('🗑 Coursier binary removed') 26 | 27 | await mill.remove() 28 | core.info('🗑 Mill binary removed') 29 | } catch (error: unknown) { 30 | core.warning((error as Error).message) 31 | } 32 | } 33 | 34 | // eslint-disable-next-line unicorn/prefer-top-level-await 35 | void run() 36 | -------------------------------------------------------------------------------- /src/action/pre.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core' 2 | import fetch from 'node-fetch' 3 | import * as coursier from '../modules/coursier' 4 | import {HealthCheck} from '../modules/healthcheck' 5 | import * as mill from '../modules/mill' 6 | 7 | /** 8 | * Runs the action prerequisites code. In order it will do the following: 9 | * 10 | * - Check connection with Maven Central 11 | * - Install Coursier 12 | * - Install Scalafmt 13 | * - Install Scalafix 14 | * - Install Mill 15 | */ 16 | async function run(): Promise { 17 | try { 18 | const healthCheck: HealthCheck = HealthCheck.from(core, {run: async url => fetch(url)}) 19 | await healthCheck.mavenCentral() 20 | 21 | await coursier.install() 22 | await mill.install() 23 | } catch (error: unknown) { 24 | core.setFailed(` ✕ ${(error as Error).message}`) 25 | } 26 | } 27 | 28 | // eslint-disable-next-line unicorn/prefer-top-level-await 29 | void run() 30 | -------------------------------------------------------------------------------- /src/core/cache.ts: -------------------------------------------------------------------------------- 1 | export type ActionCache = { 2 | /** 3 | * Restores cache from keys 4 | */ 5 | restoreCache(paths: string[], primaryKey: string, restoreKeys?: string[]): Promise; 6 | 7 | /** 8 | * Saves a list of files with the specified key 9 | */ 10 | saveCache(paths: string[], key: string): Promise; 11 | } 12 | -------------------------------------------------------------------------------- /src/core/files.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Represent file operations performed by the action 3 | */ 4 | export type Files = { 5 | /** 6 | * Changes the permissions of a file. 7 | */ 8 | chmodSync: (path: string, mode: number) => void; 9 | 10 | /** 11 | * Write file contents to the filesystem. 12 | */ 13 | writeFileSync: (path: string, content: string) => void; 14 | 15 | /** 16 | * Make a directory. Creates the full path with folders in between. 17 | */ 18 | mkdirP: (path: string) => Promise; 19 | 20 | /** 21 | * Read file contents from the filesystem. 22 | */ 23 | rmRF: (path: string) => Promise; 24 | 25 | /** 26 | * Read file contents from the filesystem. 27 | */ 28 | readFileSync: (path: string, encoding: 'utf8') => string; 29 | 30 | /** 31 | * Returns `true` if the provided path exists. 32 | */ 33 | existsSync: (path: string) => boolean; 34 | } 35 | -------------------------------------------------------------------------------- /src/core/http.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Represents an HTTP client 3 | */ 4 | export type HttpClient = { 5 | run: (url: string) => Promise; 6 | } 7 | 8 | export type Response = { 9 | ok: boolean; 10 | status: number; 11 | } 12 | -------------------------------------------------------------------------------- /src/core/logger.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Represents the logger used across the action. 3 | */ 4 | export type Logger = { 5 | startGroup(group: string): void; 6 | 7 | endGroup(): void; 8 | 9 | info(message: string): void; 10 | 11 | debug(message: string): void; 12 | 13 | error(message: string): void; 14 | 15 | warning(message: string): void; 16 | } 17 | 18 | // eslint-disable-next-line @typescript-eslint/no-redeclare 19 | export const Logger = { 20 | noOp: { 21 | info() {}, debug() {}, error() {}, warning() {}, startGroup() {}, endGroup() {}, 22 | }, 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/core/os.ts: -------------------------------------------------------------------------------- 1 | export type OSInfo = { 2 | homedir(): string; 3 | } 4 | -------------------------------------------------------------------------------- /src/core/types.ts: -------------------------------------------------------------------------------- 1 | export class NonEmptyString { 2 | static from(string: string): NonEmptyString | undefined { 3 | return (string === '') ? undefined : new NonEmptyString(string) 4 | } 5 | 6 | private constructor(readonly value: string) {} 7 | } 8 | 9 | /** 10 | * Creates a `NonEmptyString` from a `string`. Returns `undefined` if the string is empty. 11 | */ 12 | export function nonEmpty(string: string): NonEmptyString | undefined { 13 | return NonEmptyString.from(string) 14 | } 15 | 16 | /** 17 | * Creates a `NonEmptyString` from a `string`. Throws an `Error` if the string is empty. 18 | */ 19 | export function mandatory(string: string, message = `Input ${string} cannot be empty`): NonEmptyString { 20 | const value = nonEmpty(string) 21 | 22 | if (value === undefined) { 23 | throw new Error(message) 24 | } 25 | 26 | return value 27 | } 28 | -------------------------------------------------------------------------------- /src/modules/coursier.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path' 2 | import * as os from 'os' 3 | import * as core from '@actions/core' 4 | import * as tc from '@actions/tool-cache' 5 | import * as io from '@actions/io' 6 | import * as exec from '@actions/exec' 7 | import {type NonEmptyString} from '../core/types' 8 | 9 | /** 10 | * Installs `coursier` and add its executable to the `PATH`. 11 | * 12 | * Once coursier is installed, installs `scalafmt` 13 | * `scalafix`, `sbt` and `scala-cli` tools. 14 | * 15 | * Throws error if the installation fails. 16 | */ 17 | export async function install(): Promise { 18 | try { 19 | const coursierUrl = core.getInput('coursier-cli-url') 20 | 21 | core.debug(`Installing coursier from ${coursierUrl}`) 22 | 23 | const binary = path.join(os.homedir(), 'bin') 24 | await io.mkdirP(binary) 25 | 26 | const zip = await tc.downloadTool(coursierUrl, path.join(binary, 'cs.gz')) 27 | 28 | await exec.exec('gzip', ['-d', zip], {silent: true}) 29 | await exec.exec('chmod', ['+x', path.join(binary, 'cs')], {silent: true}) 30 | 31 | core.addPath(binary) 32 | 33 | await exec.exec( 34 | 'cs', 35 | ['install', 'scalafmt', 'scalafix', 'scala-cli', 'sbt', '--install-dir', binary], 36 | { 37 | silent: true, 38 | listeners: {stdline: core.debug, errline: core.debug}, 39 | }, 40 | ) 41 | 42 | const coursierVersion = await execute('cs', 'version') 43 | 44 | core.info(`✓ Coursier installed, version: ${coursierVersion.trim()}`) 45 | 46 | const scalafmtVersion = await execute('cs', 'launch', 'scalafmt', '--', '--version') 47 | 48 | core.info(`✓ Scalafmt installed, version: ${scalafmtVersion.replace(/^scalafmt /, '').trim()}`) 49 | 50 | const scalafixVersion = await execute('cs', 'launch', 'scalafix', '--', '--version') 51 | 52 | core.info(`✓ Scalafix installed, version: ${scalafixVersion.trim()}`) 53 | 54 | core.info('✓ SBT installed') 55 | 56 | core.info('✓ scala-cli installed') 57 | } catch (error: unknown) { 58 | core.debug((error as Error).message) 59 | throw new Error('Unable to install coursier or managed tools') 60 | } 61 | } 62 | 63 | /** 64 | * Launches an app using `coursier`. 65 | * 66 | * Refer to [coursier](https://get-coursier.io/docs/cli-launch) for more information. 67 | * 68 | * @param app - The application to launch 69 | * @param arguments_ - The args to pass to the application launcher. 70 | * @param extraJars - Extra JARs to be added to the classpath of the launched application. Directories accepted too. 71 | */ 72 | export async function launch( 73 | app: string, 74 | arguments_: Array = [], 75 | extraJars: NonEmptyString | undefined = undefined, 76 | ): Promise { 77 | core.startGroup(`Launching ${app}`) 78 | 79 | const launchArguments = [ 80 | 'launch', 81 | '--contrib', 82 | '-r', 83 | 'sonatype:snapshots', 84 | app, 85 | ...(extraJars ? ['--extra-jars', extraJars.value] : []), 86 | '--', 87 | ...arguments_.flatMap((argument: string | string[]) => (typeof argument === 'string' ? [argument] : argument)), 88 | ] 89 | 90 | const code = await exec.exec('cs', launchArguments, { 91 | silent: true, 92 | ignoreReturnCode: true, 93 | listeners: {stdline: core.info, errline: core.error}, 94 | }) 95 | 96 | core.endGroup() 97 | 98 | if (code !== 0) { 99 | throw new Error(`Launching ${app} failed`) 100 | } 101 | } 102 | 103 | /** 104 | * Removes coursier binary 105 | */ 106 | export async function remove(): Promise { 107 | await io.rmRF(path.join(os.homedir(), '.cache', 'coursier', 'v1')) 108 | await exec.exec('cs', ['uninstall', '--all'], { 109 | silent: true, 110 | ignoreReturnCode: true, 111 | listeners: {stdline: core.info, errline: core.debug}, 112 | }) 113 | } 114 | 115 | /** 116 | * Executes a tool and returns its output. 117 | */ 118 | async function execute(tool: string, ...arguments_: string[]): Promise { 119 | let output = '' 120 | 121 | const code = await exec.exec(tool, arguments_, { 122 | silent: true, 123 | ignoreReturnCode: true, 124 | listeners: { 125 | stdout(data) { 126 | (output += data.toString()) 127 | }, errline: core.debug, 128 | }, 129 | }) 130 | 131 | if (code !== 0) { 132 | throw new Error(`There was an error while executing '${tool} ${arguments_.join(' ')}'`) 133 | } 134 | 135 | return output 136 | } 137 | -------------------------------------------------------------------------------- /src/modules/github.test.ts: -------------------------------------------------------------------------------- 1 | import {fail} from 'assert' 2 | import test from 'ava' 3 | import {Logger} from '../core/logger' 4 | import {GitHub, type GitHubClient} from './github' 5 | 6 | test('`GitHub.getAuthUser()` → returns every auth user component', async t => { 7 | const client: GitHubClient = { 8 | rest: { 9 | users: { 10 | getByUsername: async () => fail('This should not be called'), 11 | getAuthenticated: async () => ({data: {login: 'alejandrohdezma', email: 'alex@example.com', name: 'Alex'}}), 12 | }, 13 | }, 14 | } 15 | 16 | const input = GitHub.from(Logger.noOp, client) 17 | 18 | const user = await input.getAuthUser() 19 | 20 | t.is(user.login().value, 'alejandrohdezma') 21 | t.is(user.email().value, 'alex@example.com') 22 | t.is(user.name().value, 'Alex') 23 | }) 24 | 25 | test('`GitHub.getAuthUser()` → throws error on any empty component', async t => { 26 | const client: GitHubClient = { 27 | rest: { 28 | users: { 29 | getByUsername: async () => fail('This should not be called'), 30 | getAuthenticated: async () => ({data: {login: '', email: '', name: ''}}), 31 | }, 32 | }, 33 | } 34 | 35 | const input = GitHub.from(Logger.noOp, client) 36 | 37 | const user = await input.getAuthUser() 38 | 39 | { 40 | const expected = 'Unable to retrieve user information from GitHub' 41 | t.throws(() => user.login().value, {instanceOf: Error, message: expected}) 42 | } 43 | 44 | { 45 | const expected = 'Unable to find author\'s email. Either ensure that the token\'s GitHub Account ' 46 | + 'has the email privacy feature disabled for at least one email or use the `author-email` input to provide one.' 47 | t.throws(() => user.email().value, {instanceOf: Error, message: expected}) 48 | } 49 | 50 | { 51 | const expected = 'Unable to find author\'s name. Either ensure that the token\'s GitHub Account ' 52 | + 'has a valid name set in its profile or use the `author-name` input to provide one.' 53 | t.throws(() => user.name().value, {instanceOf: Error, message: expected}) 54 | } 55 | }) 56 | 57 | test('`GitHub.getAuthUser()` → throws error on any null component', async t => { 58 | const client: GitHubClient = { 59 | rest: { 60 | users: { 61 | getByUsername: async () => fail('This should not be called'), 62 | getAuthenticated: async () => ({data: {login: 'alex', email: null, name: null}}), 63 | }, 64 | }, 65 | } 66 | 67 | const input = GitHub.from(Logger.noOp, client) 68 | 69 | const user = await input.getAuthUser() 70 | 71 | { 72 | const expected = 'Unable to find author\'s email. Either ensure that the token\'s GitHub Account ' 73 | + 'has the email privacy feature disabled for at least one email or use the `author-email` input to provide one.' 74 | t.throws(() => user.email().value, {instanceOf: Error, message: expected}) 75 | } 76 | 77 | { 78 | const expected = 'Unable to find author\'s name. Either ensure that the token\'s GitHub Account ' 79 | + 'has a valid name set in its profile or use the `author-name` input to provide one.' 80 | t.throws(() => user.name().value, {instanceOf: Error, message: expected}) 81 | } 82 | }) 83 | 84 | test('`GitHub.getAppUser()` → returns every auth user component', async t => { 85 | const client: GitHubClient = { 86 | rest: { 87 | users: { 88 | getByUsername: async () => ({data: {login: 'my-app[bot]', id: 123}}), 89 | getAuthenticated: async () => fail('This should not be called'), 90 | }, 91 | }, 92 | } 93 | 94 | const input = GitHub.from(Logger.noOp, client) 95 | 96 | const user = await input.getAppUser('the-slug') 97 | 98 | t.is(user.login().value, 'my-app[bot]') 99 | t.is(user.email().value, '123+my-app[bot]@users.noreply.github.com') 100 | t.is(user.name().value, 'my-app[bot]') 101 | }) 102 | 103 | test('`GitHub.getAppUser()` → returns default user if slug is empty', async t => { 104 | const client: GitHubClient = { 105 | rest: { 106 | users: { 107 | getByUsername: async () => ({data: {login: 'my-app[bot]', id: 123}}), 108 | getAuthenticated: async () => fail('This should not be called'), 109 | }, 110 | }, 111 | } 112 | 113 | const input = GitHub.from(Logger.noOp, client) 114 | 115 | const user = await input.getAppUser(undefined) 116 | 117 | t.is(user.login().value, 'github-actions[bot]') 118 | t.is(user.email().value, '41898282+github-actions[bot]@users.noreply.github.com') 119 | t.is(user.name().value, 'github-actions[bot]') 120 | }) 121 | 122 | test('`GitHub.getAppUser()` → returns default user if failed to obtain bot user', async t => { 123 | const client: GitHubClient = { 124 | rest: { 125 | users: { 126 | getByUsername: async () => fail('BOOM!'), 127 | getAuthenticated: async () => fail('This should not be called'), 128 | }, 129 | }, 130 | } 131 | 132 | const input = GitHub.from(Logger.noOp, client) 133 | 134 | const user = await input.getAppUser(undefined) 135 | 136 | t.is(user.login().value, 'github-actions[bot]') 137 | t.is(user.email().value, '41898282+github-actions[bot]@users.noreply.github.com') 138 | t.is(user.name().value, 'github-actions[bot]') 139 | }) 140 | -------------------------------------------------------------------------------- /src/modules/github.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/ban-types */ 2 | import {type Logger} from '../core/logger' 3 | import {mandatory, type NonEmptyString} from '../core/types' 4 | 5 | const emailErrorMessage 6 | = 'Unable to find author\'s email. Either ensure that the token\'s GitHub Account has the email ' 7 | + 'privacy feature disabled for at least one email or use the `author-email` input to provide one.' 8 | 9 | const nameErrorMessage 10 | = 'Unable to find author\'s name. Either ensure that the token\'s GitHub Account has a valid name ' 11 | + 'set in its profile or use the `author-name` input to provide one.' 12 | 13 | export class GitHub { 14 | static from( 15 | logger: Logger, 16 | github: GitHubClient, 17 | ) { 18 | return new GitHub(logger, github) 19 | } 20 | 21 | // https://github.community/t/github-actions-bot-email-address/17204/6 22 | // https://api.github.com/users/github-actions%5Bbot%5D 23 | private readonly defaultUser = { 24 | login: () => mandatory('github-actions[bot]'), 25 | email: () => mandatory('41898282+github-actions[bot]@users.noreply.github.com'), 26 | name: () => mandatory('github-actions[bot]'), 27 | } 28 | 29 | constructor( 30 | private readonly logger: Logger, 31 | private readonly github: GitHubClient, 32 | ) {} 33 | 34 | /** 35 | * Returns the login, email and name of the authenticated user. 36 | */ 37 | async getAuthUser(): Promise { 38 | try { 39 | const auth = await this.github.rest.users.getAuthenticated() 40 | const {login, email, name} = auth.data 41 | 42 | this.logger.info('✓ User information retrieved from GitHub') 43 | 44 | this.logger.debug(`- Login: ${login}`) 45 | this.logger.debug(`- Email: ${email ?? 'no email found'}`) 46 | this.logger.debug(`- Name: ${name ?? 'no name found'}`) 47 | 48 | return { 49 | login: () => mandatory(login, 'Unable to retrieve user information from GitHub'), 50 | email: () => mandatory(email ?? '', emailErrorMessage), 51 | name: () => mandatory(name ?? '', nameErrorMessage), 52 | } 53 | } catch (error: unknown) { 54 | this.logger.debug(`- User information retrieve failed. Error: ${(error as Error).message}`) 55 | return this.defaultUser 56 | } 57 | } 58 | 59 | /** 60 | * Returns the login, email and name of the authenticated user. 61 | */ 62 | async getAppUser(slug: string | undefined): Promise { 63 | try { 64 | if (slug === undefined) { 65 | throw new Error('Unable to find GitHub App Slug') 66 | } 67 | 68 | const response = await this.github.rest.users.getByUsername({username: slug + '[bot]'}) 69 | 70 | // Workaround until https://github.com/github/rest-api-description/issues/288 is fixed 71 | const {login, id} = (response as {data: {login: string; id: string}}).data 72 | 73 | this.logger.info('✓ GitHub App information retrieved from GitHub') 74 | 75 | return { 76 | login: () => mandatory(login, 'Unable to retrieve user information from GitHub'), 77 | email: () => mandatory(`${id}+${login}@users.noreply.github.com`), 78 | name: () => mandatory(login), 79 | } 80 | } catch (error: unknown) { 81 | this.logger.debug(`- GitHub App User information retrieve failed. Error: ${(error as Error).message}`) 82 | return this.defaultUser 83 | } 84 | } 85 | } 86 | 87 | type AuthUser = { 88 | email: () => NonEmptyString; 89 | login: () => NonEmptyString; 90 | name: () => NonEmptyString; 91 | } 92 | 93 | export type GitHubClient = { 94 | rest: { 95 | users: { 96 | getAuthenticated: () => Promise<{data: {login: string; email: string | null; name: string | null}}>; 97 | getByUsername: (parameters?: {username: string}) => Promise; 98 | }; 99 | }; 100 | } 101 | -------------------------------------------------------------------------------- /src/modules/healthcheck.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import {type HttpClient} from '../core/http' 3 | import {Logger} from '../core/logger' 4 | import {HealthCheck} from './healthcheck' 5 | 6 | test('`HealthCheck.mavenCentral()` → does not fail if connected to Maven Central', async t => { 7 | const client: HttpClient = { 8 | run: async (url: string) => url === 'https://repo1.maven.org/maven2/' 9 | ? {ok: true, status: 200} : {ok: false, status: 404}, 10 | } 11 | 12 | const healthCheck = HealthCheck.from(Logger.noOp, client) 13 | 14 | await t.notThrowsAsync(async () => healthCheck.mavenCentral()) 15 | }) 16 | 17 | test('`HealthCheck.mavenCentral()` → fails if not connected to Maven Central', async t => { 18 | const client: HttpClient = { 19 | run: async () => ({ok: false, status: 404}), 20 | } 21 | 22 | const healthCheck = HealthCheck.from(Logger.noOp, client) 23 | 24 | const expected = 'Unable to connect to Maven Central' 25 | 26 | await t.throwsAsync(async () => healthCheck.mavenCentral(), {instanceOf: Error, message: expected}) 27 | }) 28 | -------------------------------------------------------------------------------- /src/modules/healthcheck.ts: -------------------------------------------------------------------------------- 1 | import {type Logger} from '../core/logger' 2 | import {type HttpClient} from '../core/http' 3 | 4 | export class HealthCheck { 5 | static from(logger: Logger, httpClient: HttpClient) { 6 | return new HealthCheck(logger, httpClient) 7 | } 8 | 9 | constructor(private readonly logger: Logger, private readonly httpClient: HttpClient) {} 10 | 11 | /** 12 | * Checks connection with Maven Central, throws error if unable to connect. 13 | */ 14 | async mavenCentral(): Promise { 15 | const success = await this.httpClient.run('https://repo1.maven.org/maven2/').then(response => response.ok) 16 | 17 | if (!success) { 18 | throw new Error('Unable to connect to Maven Central') 19 | } 20 | 21 | this.logger.info('✓ Connected to Maven Central') 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/modules/input.test.ts: -------------------------------------------------------------------------------- 1 | import {fail} from 'assert' 2 | import test from 'ava' 3 | import {match} from 'ts-pattern' 4 | import {type Files} from '../core/files' 5 | import {Logger} from '../core/logger' 6 | import {nonEmpty} from '../core/types' 7 | import {Input} from './input' 8 | 9 | test('`Input.all` → returns all inputs', t => { 10 | const inputs = (name: string) => match(name) 11 | .with('github-token', () => '123') 12 | .with('repo-config', () => '.github/defaults/.scala-steward.conf') 13 | .with('github-repository', () => 'owner/repo') 14 | .with('branches', () => '1.0x,2.0x') 15 | .with('author-email', () => 'alex@example.com') 16 | .with('author-name', () => 'Alex') 17 | .with('github-api-url', () => 'github.my-org.com') 18 | .with('cache-ttl', () => '20m') 19 | .with('max-buffer-size', () => '16384') 20 | .with('timeout', () => '60s') 21 | .with('scala-steward-version', () => '1.0') 22 | .with('artifact-migrations', () => '.github/artifact-migrations.conf') 23 | .with('scalafix-migrations', () => '.github/scalafix-migrations.conf') 24 | .with('other-args', () => '--help') 25 | .with('signing-key', () => '42') 26 | .with('extra-jars', () => 'path/to/my/jars') 27 | .otherwise(() => '') 28 | 29 | const booleanInputs = (name: string) => match(name) 30 | .with('ignore-opts-files', () => true) 31 | .with('sign-commits', () => true) 32 | .otherwise(() => false) 33 | 34 | const files: Files = { 35 | chmodSync: () => fail('Should not be called'), 36 | rmRF: () => fail('Should not be called'), 37 | mkdirP: () => fail('Should not be called'), 38 | existsSync: name => name === '.github/defaults/.scala-steward.conf', 39 | writeFileSync: () => fail('Should not be called'), 40 | readFileSync: () => fail('Should not be called'), 41 | } 42 | 43 | const input = Input.from({getInput: inputs, getBooleanInput: booleanInputs}, files, Logger.noOp) 44 | 45 | const expected = { 46 | github: { 47 | token: nonEmpty('123'), 48 | app: undefined, 49 | apiUrl: nonEmpty('github.my-org.com'), 50 | }, 51 | steward: { 52 | defaultConfiguration: nonEmpty('.github/defaults/.scala-steward.conf'), 53 | repos: '- owner/repo:1.0x\n- owner/repo:2.0x', 54 | cacheTtl: nonEmpty('20m'), 55 | maxBufferSize: nonEmpty('16384'), 56 | version: nonEmpty('1.0'), 57 | timeout: nonEmpty('60s'), 58 | ignoreOptsFiles: true, 59 | extraArgs: nonEmpty('--help'), 60 | extraJars: nonEmpty('path/to/my/jars'), 61 | }, 62 | migrations: { 63 | scalafix: nonEmpty('.github/scalafix-migrations.conf'), 64 | artifacts: nonEmpty('.github/artifact-migrations.conf'), 65 | }, 66 | commits: { 67 | sign: { 68 | enabled: true, 69 | key: nonEmpty('42'), 70 | }, 71 | author: { 72 | email: nonEmpty('alex@example.com'), 73 | name: nonEmpty('Alex'), 74 | }, 75 | }, 76 | } 77 | 78 | t.deepEqual(input.all(), expected) 79 | }) 80 | 81 | test('`Input.githubAppInfo()` → returns GitHub App info', t => { 82 | const inputs = (name: string) => match(name) 83 | .with('github-app-auth-only', () => 'true') 84 | .with('github-app-id', () => '123') 85 | .with('github-app-key', () => '42') 86 | .with('github-app-installation-id', () => '456') 87 | .otherwise(() => '') 88 | 89 | const booleanInputs = (name: string) => match(name) 90 | .with('github-app-auth-only', () => true) 91 | .otherwise(() => false) 92 | 93 | const files: Files = { 94 | chmodSync: () => fail('Should not be called'), 95 | rmRF: () => fail('Should not be called'), 96 | mkdirP: () => fail('Should not be called'), 97 | existsSync: () => false, 98 | writeFileSync: () => fail('Should not be called'), 99 | readFileSync: () => fail('Should not be called'), 100 | } 101 | 102 | const input = Input.from({getInput: inputs, getBooleanInput: booleanInputs}, files, Logger.noOp) 103 | 104 | const file = input.githubAppInfo() 105 | 106 | t.deepEqual(file, { 107 | authOnly: true, id: nonEmpty('123'), key: nonEmpty('42'), installation: nonEmpty('456'), 108 | }) 109 | }) 110 | 111 | test('`Input.githubAppInfo()` → returns undefined on missing inputs', t => { 112 | const files: Files = { 113 | chmodSync: () => fail('Should not be called'), 114 | rmRF: () => fail('Should not be called'), 115 | mkdirP: () => fail('Should not be called'), 116 | existsSync: () => false, 117 | writeFileSync: () => fail('Should not be called'), 118 | readFileSync: () => fail('Should not be called'), 119 | } 120 | 121 | const input = Input.from({getInput: () => '', getBooleanInput: () => false}, files, Logger.noOp) 122 | 123 | const file = input.githubAppInfo() 124 | 125 | t.is(file, undefined) 126 | }) 127 | 128 | test('`Input.githubAppInfo()` → throws error if only id input present', t => { 129 | const inputs = (name: string) => match(name) 130 | .with('github-app-id', () => '123') 131 | .otherwise(() => '') 132 | 133 | const files: Files = { 134 | chmodSync: () => fail('Should not be called'), 135 | rmRF: () => fail('Should not be called'), 136 | mkdirP: () => fail('Should not be called'), 137 | existsSync: () => false, 138 | writeFileSync: () => fail('Should not be called'), 139 | readFileSync: () => fail('Should not be called'), 140 | } 141 | 142 | const input = Input.from({getInput: inputs, getBooleanInput: () => false}, files, Logger.noOp) 143 | 144 | const expected = '`github-app-id` and `github-app-key` inputs have to be set together. One of them is missing' 145 | 146 | const error = t.throws(() => input.githubAppInfo(), {instanceOf: Error}) 147 | 148 | t.is(error?.message, expected) 149 | }) 150 | 151 | test('`Input.githubAppInfo()` → throws error if only key input present', t => { 152 | const inputs = (name: string) => match(name) 153 | .with('github-app-key', () => '42') 154 | .otherwise(() => '') 155 | 156 | const files: Files = { 157 | chmodSync: () => fail('Should not be called'), 158 | rmRF: () => fail('Should not be called'), 159 | mkdirP: () => fail('Should not be called'), 160 | existsSync: () => false, 161 | writeFileSync: () => fail('Should not be called'), 162 | readFileSync: () => fail('Should not be called'), 163 | } 164 | 165 | const input = Input.from({getInput: inputs, getBooleanInput: () => false}, files, Logger.noOp) 166 | 167 | const expected = '`github-app-id` and `github-app-key` inputs have to be set together. One of them is missing' 168 | 169 | const error = t.throws(() => input.githubAppInfo(), {instanceOf: Error}) 170 | 171 | t.is(error?.message, expected) 172 | }) 173 | 174 | test('`Input.reposFile()` → returns undefined on missing input', t => { 175 | const files: Files = { 176 | chmodSync: () => fail('Should not be called'), 177 | rmRF: () => fail('Should not be called'), 178 | mkdirP: () => fail('Should not be called'), 179 | existsSync: () => false, 180 | writeFileSync: () => fail('Should not be called'), 181 | readFileSync: () => fail('Should not be called'), 182 | } 183 | 184 | const input = Input.from({getInput: () => '', getBooleanInput: () => false}, files, Logger.noOp) 185 | 186 | const file = input.reposFile() 187 | t.is(file, undefined) 188 | }) 189 | 190 | test('`Input.reposFile()` → returns contents if file exists', t => { 191 | const inputs = (name: string) => match(name) 192 | .with('repos-file', () => 'repos.md') 193 | .otherwise(() => '') 194 | 195 | const contents = '- owner1/repo1\n- owner1/repo2\n- owner2/repo' 196 | 197 | const files: Files = { 198 | chmodSync: () => fail('Should not be called'), 199 | rmRF: () => fail('Should not be called'), 200 | mkdirP: () => fail('Should not be called'), 201 | existsSync: name => match(name).with('repos.md', () => true).run(), 202 | writeFileSync: () => fail('Should not be called'), 203 | readFileSync: name => match(name).with('repos.md', () => contents).run(), 204 | } 205 | 206 | const input = Input.from({getInput: inputs, getBooleanInput: () => false}, files, Logger.noOp) 207 | 208 | const file = input.reposFile() ?? '' 209 | 210 | t.is(file.toString(), contents) 211 | }) 212 | 213 | test('`Input.reposFile()` → throws error if file does not exist', t => { 214 | const inputs = (name: string) => match(name) 215 | .with('repos-file', () => 'this/does/not/exist.md') 216 | .otherwise(() => '') 217 | 218 | const files: Files = { 219 | chmodSync: () => fail('Should not be called'), 220 | rmRF: () => fail('Should not be called'), 221 | mkdirP: () => fail('Should not be called'), 222 | existsSync: () => false, 223 | writeFileSync: () => fail('Should not be called'), 224 | readFileSync: () => fail('Should not be called'), 225 | } 226 | 227 | const input = Input.from({getInput: inputs, getBooleanInput: () => false}, files, Logger.noOp) 228 | 229 | const expected = 'The path indicated in `repos-file` (this/does/not/exist.md) does not exist' 230 | 231 | const error = t.throws(() => input.reposFile(), {instanceOf: Error}) 232 | 233 | t.is(error?.message, expected) 234 | }) 235 | 236 | test('`Input.githubRepository()` → returns repository from input', t => { 237 | const inputs = (name: string) => match(name) 238 | .with('github-repository', () => 'owner/repo') 239 | .otherwise(() => '') 240 | 241 | const files: Files = { 242 | chmodSync: () => fail('Should not be called'), 243 | rmRF: () => fail('Should not be called'), 244 | mkdirP: () => fail('Should not be called'), 245 | existsSync: () => false, 246 | writeFileSync: () => fail('Should not be called'), 247 | readFileSync: () => fail('Should not be called'), 248 | } 249 | 250 | const input = Input.from({getInput: inputs, getBooleanInput: () => false}, files, Logger.noOp) 251 | 252 | const content = input.githubRepository() 253 | 254 | const expected = '- owner/repo' 255 | 256 | t.is(content, expected) 257 | }) 258 | 259 | test('`Input.githubRepository()` → returns repository from input with custom branch', t => { 260 | const inputs = (name: string) => match(name) 261 | .with('github-repository', () => 'owner/repo') 262 | .with('branches', () => '0.1.x') 263 | .otherwise(() => '') 264 | 265 | const files: Files = { 266 | chmodSync: () => fail('Should not be called'), 267 | rmRF: () => fail('Should not be called'), 268 | mkdirP: () => fail('Should not be called'), 269 | existsSync: () => false, 270 | writeFileSync: () => fail('Should not be called'), 271 | readFileSync: () => fail('Should not be called'), 272 | } 273 | 274 | const input = Input.from({getInput: inputs, getBooleanInput: () => false}, files, Logger.noOp) 275 | 276 | const content = input.githubRepository() 277 | 278 | const expected = '- owner/repo:0.1.x' 279 | 280 | t.is(content, expected) 281 | }) 282 | 283 | test('`Input.githubRepository()` → returns repository from input with multiple custom branches', t => { 284 | const inputs = (name: string) => match(name) 285 | .with('github-repository', () => 'owner/repo') 286 | .with('branches', () => 'main,0.1.x,0.2.x') 287 | .otherwise(() => '') 288 | 289 | const files: Files = { 290 | chmodSync: () => fail('Should not be called'), 291 | rmRF: () => fail('Should not be called'), 292 | mkdirP: () => fail('Should not be called'), 293 | existsSync: () => false, 294 | writeFileSync: () => fail('Should not be called'), 295 | readFileSync: () => fail('Should not be called'), 296 | } 297 | 298 | const input = Input.from({getInput: inputs, getBooleanInput: () => false}, files, Logger.noOp) 299 | 300 | const content = input.githubRepository() 301 | 302 | const expected = '- owner/repo:main\n- owner/repo:0.1.x\n- owner/repo:0.2.x' 303 | 304 | t.is(content, expected) 305 | }) 306 | 307 | test('`Input.defaultRepoConf()` → returns the path if it exists', t => { 308 | const inputs = (name: string) => match(name) 309 | .with('repo-config', () => '.scala-steward.conf') 310 | .otherwise(() => '') 311 | 312 | const files: Files = { 313 | chmodSync: () => fail('Should not be called'), 314 | rmRF: () => fail('Should not be called'), 315 | mkdirP: () => fail('Should not be called'), 316 | existsSync: name => match(name).with('.scala-steward.conf', () => true).run(), 317 | writeFileSync: () => fail('Should not be called'), 318 | readFileSync: () => fail('This should not be called'), 319 | } 320 | 321 | const input = Input.from({getInput: inputs, getBooleanInput: () => false}, files, Logger.noOp) 322 | 323 | const path = input.defaultRepoConf() 324 | 325 | const expected = '.scala-steward.conf' 326 | 327 | t.is(path?.value, expected) 328 | }) 329 | 330 | test('`Input.defaultRepoConf()` → returns the default path if it exists', t => { 331 | const inputs = (name: string) => match(name) 332 | .with('repo-config', () => '.github/.scala-steward.conf') 333 | .otherwise(() => '') 334 | 335 | const files: Files = { 336 | chmodSync: () => fail('Should not be called'), 337 | rmRF: () => fail('Should not be called'), 338 | mkdirP: () => fail('Should not be called'), 339 | existsSync: name => match(name).with('.github/.scala-steward.conf', () => true).run(), 340 | writeFileSync: () => fail('Should not be called'), 341 | readFileSync: () => fail('This should not be called'), 342 | } 343 | 344 | const input = Input.from({getInput: inputs, getBooleanInput: () => false}, files, Logger.noOp) 345 | 346 | const path = input.defaultRepoConf() 347 | 348 | const expected = '.github/.scala-steward.conf' 349 | 350 | t.is(path?.value, expected) 351 | }) 352 | 353 | test('`Input.defaultRepoConf()` → returns undefined if the default path does not exist', t => { 354 | const inputs = (name: string) => match(name) 355 | .with('repo-config', () => '.github/.scala-steward.conf') 356 | .otherwise(() => '') 357 | 358 | const files: Files = { 359 | chmodSync: () => fail('Should not be called'), 360 | rmRF: () => fail('Should not be called'), 361 | mkdirP: () => fail('Should not be called'), 362 | existsSync: () => false, 363 | writeFileSync: () => fail('Should not be called'), 364 | readFileSync: () => fail('Should not be called'), 365 | } 366 | 367 | const input = Input.from({getInput: inputs, getBooleanInput: () => false}, files, Logger.noOp) 368 | 369 | const path = input.defaultRepoConf() 370 | 371 | t.is(path, undefined) 372 | }) 373 | 374 | test('`Input.defaultRepoConf()` → throws error if provided non-default file does not exist', t => { 375 | const inputs = (name: string) => match(name) 376 | .with('repo-config', () => 'tests/resources/.scala-steward-new.conf') 377 | .otherwise(() => '') 378 | 379 | const files: Files = { 380 | chmodSync: () => fail('Should not be called'), 381 | rmRF: () => fail('Should not be called'), 382 | mkdirP: () => fail('Should not be called'), 383 | existsSync: () => false, 384 | writeFileSync: () => fail('Should not be called'), 385 | readFileSync: () => fail('Should not be called'), 386 | } 387 | 388 | const input = Input.from({getInput: inputs, getBooleanInput: () => false}, files, Logger.noOp) 389 | 390 | const expected = 'Provided default repo conf file (tests/resources/.scala-steward-new.conf) does not exist' 391 | 392 | const error = t.throws(() => input.defaultRepoConf(), {instanceOf: Error}) 393 | 394 | t.is(error?.message, expected) 395 | }) 396 | -------------------------------------------------------------------------------- /src/modules/input.ts: -------------------------------------------------------------------------------- 1 | 2 | import {type Files} from '../core/files' 3 | import {type Logger} from '../core/logger' 4 | import {mandatory, nonEmpty, type NonEmptyString} from '../core/types' 5 | 6 | export type GitHubAppInfo = { 7 | authOnly: boolean; 8 | id: NonEmptyString; 9 | installation: NonEmptyString | undefined; 10 | key: NonEmptyString; 11 | } 12 | 13 | /** 14 | * Retrieves (and sanitize) inputs. 15 | */ 16 | export class Input { 17 | static from( 18 | inputs: {getInput: (name: string) => string; getBooleanInput: (name: string) => boolean}, 19 | files: Files, 20 | logger: Logger, 21 | ) { 22 | return new Input(inputs, files, logger) 23 | } 24 | 25 | constructor( 26 | private readonly inputs: {getInput: (name: string) => string; getBooleanInput: (name: string) => boolean}, 27 | private readonly files: Files, 28 | private readonly logger: Logger, 29 | ) {} 30 | 31 | /** 32 | * Returns every input for this action. 33 | */ 34 | all() { 35 | return { 36 | github: { 37 | token: mandatory(this.inputs.getInput('github-token')), 38 | app: this.githubAppInfo(), 39 | apiUrl: mandatory(this.inputs.getInput('github-api-url')), 40 | }, 41 | steward: { 42 | defaultConfiguration: this.defaultRepoConf(), 43 | repos: this.reposFile() ?? this.githubRepository(), 44 | cacheTtl: nonEmpty(this.inputs.getInput('cache-ttl')), 45 | maxBufferSize: nonEmpty(this.inputs.getInput('max-buffer-size')), 46 | version: nonEmpty(this.inputs.getInput('scala-steward-version')), 47 | timeout: nonEmpty(this.inputs.getInput('timeout')), 48 | ignoreOptsFiles: this.inputs.getBooleanInput('ignore-opts-files'), 49 | extraArgs: nonEmpty(this.inputs.getInput('other-args')), 50 | extraJars: nonEmpty(this.inputs.getInput('extra-jars')), 51 | }, 52 | migrations: { 53 | scalafix: nonEmpty(this.inputs.getInput('scalafix-migrations')), 54 | artifacts: nonEmpty(this.inputs.getInput('artifact-migrations')), 55 | }, 56 | commits: { 57 | sign: { 58 | enabled: this.inputs.getBooleanInput('sign-commits'), 59 | key: nonEmpty(this.inputs.getInput('signing-key')), 60 | }, 61 | author: { 62 | email: nonEmpty(this.inputs.getInput('author-email')), 63 | name: nonEmpty(this.inputs.getInput('author-name')), 64 | }, 65 | }, 66 | } 67 | } 68 | 69 | /** 70 | * Reads the path of the file containing the default Scala Steward configuration. 71 | * 72 | * If the provided file does not exist and is not the default one it will throw an error. 73 | * On the other hand, if it exists it will be returned, otherwise; it will return `undefined`. 74 | * 75 | * @returns {string | undefined} The path indicated in the `repo-config` input, if it 76 | * exists; otherwise, `undefined`. 77 | */ 78 | defaultRepoConf(): NonEmptyString | undefined { 79 | const path = nonEmpty(this.inputs.getInput('repo-config')) 80 | 81 | if (!path) { 82 | return undefined 83 | } 84 | 85 | const fileExists = this.files.existsSync(path.value) 86 | 87 | if (!fileExists && path.value !== '.github/.scala-steward.conf') { 88 | throw new Error(`Provided default repo conf file (${path.value}) does not exist`) 89 | } 90 | 91 | if (fileExists) { 92 | this.logger.info(`✓ Default Scala Steward configuration set to: ${path.value}`) 93 | 94 | return path 95 | } 96 | 97 | return undefined 98 | } 99 | 100 | /** 101 | * Returns the GitHub repository set to update. 102 | * 103 | * It reads it from the `github-repository` input. 104 | * 105 | * Throws error if input is empty or missing. 106 | * 107 | * If the `branches` input is set, the selected branches will be added. 108 | * 109 | * @returns {string} The GitHub repository read from the `github-repository` input. 110 | */ 111 | githubRepository(): string { 112 | const repo = nonEmpty(this.inputs.getInput('github-repository')) 113 | 114 | if (!repo) { 115 | throw new Error('Unable to read GitHub repository from `github-repository` input') 116 | } 117 | 118 | const branches = this.inputs.getInput('branches').split(',').filter(Boolean) 119 | 120 | if (branches.length === 1) { 121 | const branch = branches[0] 122 | 123 | this.logger.info(`✓ GitHub Repository set to: ${repo.value}. Will update ${branch} branch.`) 124 | 125 | return `- ${repo.value}:${branch}` 126 | } 127 | 128 | if (branches.length > 1) { 129 | this.logger.info(`✓ GitHub Repository set to: ${repo.value}. Will update ${branches.join(', ')} branches.`) 130 | 131 | return branches.map((branch: string) => `- ${repo.value}:${branch}`).join('\n') 132 | } 133 | 134 | return `- ${repo.value}` 135 | } 136 | 137 | /** 138 | * Reads the path of the file containing the list of repositories to update from the `repos-file` 139 | * input. 140 | * 141 | * If the input isn't provided this function will return `undefined`. 142 | * On the other hand, if it is provided, it will check if the path exists: 143 | * - If the file exists, its contents will be returned. 144 | * - If it doesn't exists, an error will be thrown. 145 | * 146 | * @returns {string | undefined} The contents of the file indicated in `repos-file` input, if is 147 | * defined; otherwise, `undefined`. 148 | */ 149 | reposFile(): string | undefined { 150 | const file = nonEmpty(this.inputs.getInput('repos-file')) 151 | 152 | if (!file) { 153 | return undefined 154 | } 155 | 156 | if (this.files.existsSync(file.value)) { 157 | this.logger.info(`✓ Using multiple repos file: ${file.value}`) 158 | 159 | return this.files.readFileSync(file.value, 'utf8') 160 | } 161 | 162 | throw new Error(`The path indicated in \`repos-file\` (${file.value}) does not exist`) 163 | } 164 | 165 | /** 166 | * Checks that GitHub App ID and private key are set together. 167 | * 168 | * Throws error if only one of the two inputs is set. 169 | * 170 | * @returns {{id: string, key: string} | undefined} App ID and key or undefined if both inputs are empty. 171 | */ 172 | githubAppInfo(): GitHubAppInfo | undefined { 173 | const authOnly = this.inputs.getBooleanInput('github-app-auth-only') 174 | const id = nonEmpty(this.inputs.getInput('github-app-id')) 175 | const installation = nonEmpty(this.inputs.getInput('github-app-installation-id')) 176 | const key = nonEmpty(this.inputs.getInput('github-app-key')?.replace(/\\n/g, '\n')) 177 | 178 | if (!id && !key) { 179 | return undefined 180 | } 181 | 182 | if (id && key) { 183 | return { 184 | authOnly, id, installation, key, 185 | } 186 | } 187 | 188 | throw new Error('`github-app-id` and `github-app-key` inputs have to be set together. One of them is missing') 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /src/modules/mill.ts: -------------------------------------------------------------------------------- 1 | import * as os from 'os' 2 | import * as path from 'path' 3 | import * as core from '@actions/core' 4 | import * as io from '@actions/io' 5 | import * as tc from '@actions/tool-cache' 6 | import * as exec from '@actions/exec' 7 | 8 | /** 9 | * Installs `Mill` and add its executable to the `PATH`. 10 | * 11 | * Throws error if the installation fails. 12 | */ 13 | export async function install(): Promise { 14 | try { 15 | const millVersion = core.getInput('mill-version') 16 | 17 | const cachedPath = tc.find('mill', millVersion) 18 | 19 | if (cachedPath) { 20 | core.addPath(cachedPath) 21 | } else { 22 | const millUrl = `https://github.com/lihaoyi/mill/releases/download/${millVersion}/${millVersion}` 23 | 24 | core.debug(`Attempting to install Mill from ${millUrl}`) 25 | 26 | const binary = path.join(os.homedir(), 'bin') 27 | await io.mkdirP(binary) 28 | 29 | const mill = await tc.downloadTool(millUrl, path.join(binary, 'mill')) 30 | 31 | await exec.exec('chmod', ['+x', mill], {silent: true, ignoreReturnCode: true}) 32 | 33 | await tc.cacheFile(mill, 'mill', 'mill', millVersion) 34 | } 35 | 36 | core.info(`✓ Mill installed, version: ${millVersion}`) 37 | } catch (error: unknown) { 38 | core.error((error as Error).message) 39 | throw new Error('Unable to install Mill') 40 | } 41 | } 42 | 43 | /** 44 | * Removes Mill binary 45 | */ 46 | export async function remove(): Promise { 47 | await io.rmRF(path.join(path.join(os.homedir(), 'bin'), 'mill')) 48 | } 49 | -------------------------------------------------------------------------------- /src/modules/workspace.test.ts: -------------------------------------------------------------------------------- 1 | import {fail} from 'assert' 2 | import test from 'ava' 3 | import * as sinon from 'sinon' 4 | import {type ActionCache} from '../core/cache' 5 | import {type Files} from '../core/files' 6 | import {Logger} from '../core/logger' 7 | import {mandatory} from '../core/types' 8 | import {Workspace} from './workspace' 9 | 10 | function fixture(repos_md = '') { 11 | const calls: string[] = [] 12 | 13 | const files: Files = { 14 | async chmodSync(path, mode) { 15 | calls.push(`chmodSync("${path}", ${mode})`) 16 | }, 17 | async mkdirP(path) { 18 | calls.push(`mkdirP("${path}")`) 19 | }, 20 | async writeFileSync(path, content) { 21 | calls.push(`writeFileSync("${path}", "${content}")`) 22 | }, 23 | existsSync: path => fail(`existsSync(${path}) should not be called`), 24 | async rmRF(path) { 25 | calls.push(`rmRF("${path}")`) 26 | }, 27 | readFileSync(path) { 28 | calls.push(`readFileSync("${path}")`) 29 | return repos_md 30 | }, 31 | } 32 | 33 | const os = {homedir: () => '/home/'} 34 | 35 | const cache: ActionCache = { 36 | async restoreCache(paths, primaryKey, restoreKeys) { 37 | calls.push(`restoreCache([${paths.toString()}], "${primaryKey}", [${restoreKeys?.toString() ?? ''}])`) 38 | return 'hit' 39 | }, 40 | async saveCache(paths, key) { 41 | return calls.push(`saveCache([${paths.toString()}], "${key}")`) 42 | }, 43 | } 44 | 45 | const workspace = Workspace.from(Logger.noOp, files, os, cache) 46 | 47 | return {workspace, calls} 48 | } 49 | 50 | test.before(() => { 51 | sinon.useFakeTimers() 52 | }) 53 | 54 | test.after(() => { 55 | sinon.restore() 56 | }) 57 | 58 | test('`Workspace.prepare()` → prepares the workspace', async t => { 59 | const {workspace, calls} = fixture() 60 | 61 | await workspace.prepare('- owner/repo1\n- owner/repo2', async () => '123', undefined) 62 | 63 | const expected: string[] = [ 64 | 'mkdirP("/home/scala-steward")', 65 | 'writeFileSync("/home/scala-steward/repos.md", "- owner/repo1\n- owner/repo2")', 66 | 'writeFileSync("/home/scala-steward/askpass.sh", "#!/bin/sh\n\necho \'123\'")', 67 | 'chmodSync("/home/scala-steward/askpass.sh", 493)', 68 | ] 69 | 70 | t.deepEqual(calls, expected) 71 | }) 72 | 73 | test('`Workspace.prepare()` → prepares the workspace when using a GitHub App', async t => { 74 | const {workspace, calls} = fixture() 75 | 76 | const gitHubAppInfo = { 77 | authOnly: false, 78 | id: mandatory('this-is-the-id'), 79 | installation: mandatory('this-is-the-installation-id'), 80 | key: mandatory('this-is-the-key'), 81 | } 82 | 83 | await workspace.prepare('this will not be used', async () => '123', gitHubAppInfo) 84 | 85 | const expected: string[] = [ 86 | 'mkdirP("/home/scala-steward")', 87 | 'writeFileSync("/home/scala-steward/repos.md", "")', 88 | 'writeFileSync("/home/scala-steward/app.pem", "this-is-the-key")', 89 | 'writeFileSync("/home/scala-steward/askpass.sh", "#!/bin/sh\n\necho \'123\'")', 90 | 'chmodSync("/home/scala-steward/askpass.sh", 493)', 91 | ] 92 | 93 | t.deepEqual(calls, expected) 94 | }) 95 | 96 | test('`Workspace.prepare()` → uses the repos input when GitHub App is "auth only"', async t => { 97 | const {workspace, calls} = fixture() 98 | 99 | const gitHubAppInfo = { 100 | authOnly: true, 101 | id: mandatory('this-is-the-id'), 102 | installation: mandatory('this-is-the-installation-id'), 103 | key: mandatory('this-is-the-key'), 104 | } 105 | 106 | await workspace.prepare('- owner/repo', async () => '123', gitHubAppInfo) 107 | 108 | const expected: string[] = [ 109 | 'mkdirP("/home/scala-steward")', 110 | 'writeFileSync("/home/scala-steward/repos.md", "- owner/repo")', 111 | 'writeFileSync("/home/scala-steward/askpass.sh", "#!/bin/sh\n\necho \'123\'")', 112 | 'chmodSync("/home/scala-steward/askpass.sh", 493)', 113 | ] 114 | 115 | t.deepEqual(calls, expected) 116 | }) 117 | 118 | test('`Workspace.writeAskPass()` → writes a token to the askpass.sh', async t => { 119 | const {workspace, calls} = fixture() 120 | 121 | await workspace.writeAskPass(async () => '123') 122 | 123 | const expected: string[] = [ 124 | 'writeFileSync("/home/scala-steward/askpass.sh", "#!/bin/sh\n\necho \'123\'")', 125 | ] 126 | 127 | t.deepEqual(calls, expected) 128 | }) 129 | 130 | test('`Workspace.remove()` → removes the workspace', async t => { 131 | const {workspace, calls} = fixture() 132 | 133 | await workspace.remove() 134 | 135 | const expected: string[] = [ 136 | 'rmRF("/home/scala-steward")', 137 | ] 138 | 139 | t.deepEqual(calls, expected) 140 | }) 141 | 142 | test('`Workspace.restoreWorkspaceCache()` → tries to restore the workspace cache', async t => { 143 | const {workspace, calls} = fixture('- owner/repo') 144 | 145 | await workspace.restoreWorkspaceCache() 146 | 147 | const now = Date.now() 148 | 149 | const expected: string[] = [ 150 | 'readFileSync("/home/scala-steward/repos.md")', 151 | `restoreCache([/home/scala-steward/workspace], "scala-steward-acc000fd-${now}", [scala-steward-acc000fd,scala-steward-])`, 152 | ] 153 | 154 | t.deepEqual(calls, expected) 155 | }) 156 | 157 | test('`Workspace.restoreWorkspaceCache()` → generates same hash for same contents', async t => { 158 | const {workspace, calls} = fixture('- owner/repo') 159 | 160 | await workspace.restoreWorkspaceCache() 161 | 162 | const now = Date.now() 163 | 164 | const expected: string[] = [ 165 | 'readFileSync("/home/scala-steward/repos.md")', 166 | `restoreCache([/home/scala-steward/workspace], "scala-steward-acc000fd-${now}", [scala-steward-acc000fd,scala-steward-])`, 167 | ] 168 | 169 | t.deepEqual(calls, expected) 170 | }) 171 | 172 | test('`Workspace.restoreWorkspaceCache()` → generates different hash for different contents', async t => { 173 | const {workspace, calls} = fixture('- owner/repo1') 174 | 175 | await workspace.restoreWorkspaceCache() 176 | 177 | const now = Date.now() 178 | 179 | const expected: string[] = [ 180 | 'readFileSync("/home/scala-steward/repos.md")', 181 | `restoreCache([/home/scala-steward/workspace], "scala-steward-fe470d28-${now}", [scala-steward-fe470d28,scala-steward-])`, 182 | ] 183 | 184 | t.deepEqual(calls, expected) 185 | }) 186 | 187 | test('`Workspace.saveWorkspaceCache()` → saves cache', async t => { 188 | const {workspace, calls} = fixture('- owner/repo') 189 | 190 | await workspace.purgeTempFilesAndSaveCache() 191 | 192 | const now = Date.now() 193 | 194 | const expected: string[] = [ 195 | 'rmRF("/home/scala-steward/workspace/store/refresh_error")', 196 | 'rmRF("/home/scala-steward/workspace/repos")', 197 | 'rmRF("/home/scala-steward/workspace/run-summary.md")', 198 | 'readFileSync("/home/scala-steward/repos.md")', 199 | `saveCache([/home/scala-steward/workspace], "scala-steward-acc000fd-${now}")`, 200 | ] 201 | 202 | t.deepEqual(calls, expected) 203 | }) 204 | -------------------------------------------------------------------------------- /src/modules/workspace.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import jsSHA from 'jssha/dist/sha256' 3 | import {type ActionCache} from '../core/cache' 4 | import {type Files} from '../core/files' 5 | import {type Logger} from '../core/logger' 6 | import {type OSInfo} from '../core/os' 7 | import {mandatory, type NonEmptyString} from '../core/types' 8 | import {type GitHubAppInfo} from './input' 9 | 10 | export class Workspace { 11 | static from( 12 | logger: Logger, 13 | files: Files, 14 | os: OSInfo, 15 | cache: ActionCache, 16 | ) { 17 | return new Workspace(logger, files, os, cache) 18 | } 19 | 20 | readonly directory: string 21 | readonly workspace: NonEmptyString 22 | readonly repos_md: NonEmptyString 23 | readonly app_pem: NonEmptyString 24 | readonly askpass_sh: NonEmptyString 25 | readonly runSummary_md: string 26 | 27 | private intervalId: NodeJS.Timeout | undefined 28 | 29 | constructor( 30 | private readonly logger: Logger, 31 | private readonly files: Files, 32 | private readonly os: OSInfo, 33 | private readonly cache: ActionCache, 34 | ) { 35 | this.directory = path.join(this.os.homedir(), 'scala-steward') 36 | this.workspace = mandatory(path.join(this.directory, 'workspace')) 37 | this.repos_md = mandatory(path.join(this.directory, 'repos.md')) 38 | this.app_pem = mandatory(path.join(this.directory, 'app.pem')) 39 | this.askpass_sh = mandatory(path.join(this.directory, 'askpass.sh')) 40 | this.runSummary_md = path.join(this.workspace.value, 'run-summary.md') 41 | } 42 | 43 | /** 44 | * Tries to restore the Scala Steward workspace build from the cache, if any. 45 | */ 46 | async restoreWorkspaceCache(): Promise { 47 | try { 48 | this.logger.startGroup('Trying to restore workspace contents from cache...') 49 | 50 | const hash = this.hashFile(this.repos_md.value) 51 | 52 | const cacheHit = await this.cache.restoreCache( 53 | [this.workspace.value], 54 | `scala-steward-${hash}-${Date.now().toString()}`, 55 | [`scala-steward-${hash}`, 'scala-steward-'], 56 | ) 57 | 58 | if (cacheHit) { 59 | this.logger.info('Scala Steward workspace contents restored from cache') 60 | } else { 61 | this.logger.info('Scala Steward workspace contents weren\'t found on cache') 62 | } 63 | 64 | this.logger.endGroup() 65 | } catch (error: unknown) { 66 | this.logger.debug((error as Error).message) 67 | this.logger.warning('Unable to restore workspace from cache') 68 | this.logger.endGroup() 69 | } 70 | } 71 | 72 | /** 73 | * Tries to save the Scala Steward workspace build to the cache. 74 | * 75 | * @param {string} workspace - the Scala Steward workspace directory 76 | */ 77 | async purgeTempFilesAndSaveCache(): Promise { 78 | try { 79 | this.logger.startGroup('Saving workspace to cache...') 80 | 81 | // We don't want to keep `workspace/store/refresh_error` nor `workspace/repos` in the cache. 82 | await this.files.rmRF(path.join(this.workspace.value, 'store', 'refresh_error')) 83 | await this.files.rmRF(path.join(this.workspace.value, 'repos')) 84 | 85 | // Don't persist a summary that's specific to this run 86 | await this.files.rmRF(this.runSummary_md) 87 | 88 | const hash = this.hashFile(this.repos_md.value) 89 | 90 | await this.cache.saveCache([this.workspace.value], `scala-steward-${hash}-${Date.now().toString()}`) 91 | 92 | this.logger.info('Scala Steward workspace contents saved to cache') 93 | this.logger.endGroup() 94 | } catch (error: unknown) { 95 | this.logger.debug((error as Error).message) 96 | this.logger.warning('Unable to save workspace to cache') 97 | this.logger.endGroup() 98 | } 99 | } 100 | 101 | /** 102 | * Prepares the Scala Steward workspace that will be used when launching the app. 103 | * 104 | * This will involve: 105 | * - Creating a folder `scala-steward` in the "HOME" directory. 106 | * - Creating a `repos.md` file inside workspace containing the repository/repositories to update. 107 | * - Creating a `app.pem` with the GitHub App key (if applicable). 108 | * - Creating a `askpass.sh` file inside workspace containing the GitHub token. 109 | * - Making the previous file executable. 110 | * 111 | * @param reposList The Markdown list of repositories to write to the `repos.md` file. It is only used if no 112 | * GitHub App key is provided on `gitHubAppKey` parameter. 113 | * @param token The GitHub Token used to authenticate into GitHub. 114 | * @param gitHubAppInfo The GitHub App information as provided by the user. 115 | */ 116 | async prepare(reposList: string, token: () => Promise, gitHubAppInfo: GitHubAppInfo | undefined): Promise { 117 | try { 118 | await this.files.mkdirP(this.directory) 119 | 120 | if (gitHubAppInfo && !gitHubAppInfo.authOnly) { 121 | this.files.writeFileSync(this.repos_md.value, '') 122 | this.files.writeFileSync(this.app_pem.value, gitHubAppInfo.key.value) 123 | } else { 124 | this.files.writeFileSync(this.repos_md.value, reposList) 125 | } 126 | 127 | await this.writeAskPass(token) 128 | this.intervalId = setInterval(async () => { 129 | await this.writeAskPass(token) 130 | this.logger.info('✓ GitHub Token refreshed') 131 | }, 1000 * 60 * 50) 132 | 133 | this.files.chmodSync(this.askpass_sh.value, 0o755) 134 | 135 | this.logger.info('✓ Scala Steward workspace created') 136 | } catch (error: unknown) { 137 | this.logger.debug((error as Error).message) 138 | throw new Error('Unable to create Scala Steward workspace') 139 | } 140 | } 141 | 142 | /** 143 | * Removes the Scala Steward's workspace. 144 | */ 145 | async remove(): Promise { 146 | await this.files.rmRF(this.directory) 147 | } 148 | 149 | /** 150 | * Writes a GitHub Token to the git-ask-pass file 151 | * 152 | * @param fetchToken - A function that returns a Promise resolving to a new token string. 153 | */ 154 | async writeAskPass(fetchToken: () => Promise): Promise { 155 | const token = await fetchToken() 156 | this.files.writeFileSync(this.askpass_sh.value, `#!/bin/sh\n\necho '${token}'`) 157 | } 158 | 159 | /** 160 | * Cancels the token refresh if it is currently scheduled. 161 | */ 162 | async cancelTokenRefresh(): Promise { 163 | if (this.intervalId) { 164 | clearInterval(this.intervalId) 165 | } 166 | } 167 | 168 | /** 169 | * Gets the first eight characters of the SHA-256 hash value for the 170 | * provided file's contents. 171 | * 172 | * @param {string} file - the file for which to calculate the hash 173 | * @returns {string} the file content's hash 174 | */ 175 | private hashFile(file: string): string { 176 | // eslint-disable-next-line unicorn/text-encoding-identifier-case 177 | const sha = new jsSHA('SHA-256', 'TEXT', {encoding: 'UTF8'}) 178 | sha.update(this.files.readFileSync(file, 'utf8')) 179 | return sha.getHash('HEX').slice(0, 8) 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /src/utils/docs.ts: -------------------------------------------------------------------------------- 1 | // Copy inputs from `action.yml` into `README.md` 2 | 3 | import * as fs from 'fs' 4 | import * as yaml from 'js-yaml' 5 | 6 | /** 7 | * Copy inputs from `action.yml` 8 | */ 9 | type ActionYaml = {inputs: Record} 10 | 11 | const actionYaml = yaml.load(fs.readFileSync('action.yml', {encoding: 'utf8'})) as ActionYaml 12 | 13 | const inputs = Object.entries(actionYaml.inputs).flatMap(input => 14 | [ 15 | '', 16 | ...input[1].description.trimEnd().split('\n').map(line => ` # ${line}`), 17 | ...(input[1].default ? [' #', ` # Default: ${input[1].default}`] : []), 18 | ` ${input[0]}: ''`, 19 | ], 20 | ) 21 | 22 | /** 23 | * Update `README.md` 24 | */ 25 | const readme = fs.readFileSync('README.md', {encoding: 'utf8'}) 26 | 27 | const start = readme.indexOf('') + ''.length 28 | const end = readme.indexOf('') 29 | 30 | const content = [ 31 | readme.slice(0, start), 32 | '```yaml', 33 | '- uses: scala-steward-org/scala-steward-action@v2', 34 | ' with:', 35 | ...inputs.slice(1), 36 | '```', 37 | readme.slice(end), 38 | ].join('\n') 39 | 40 | fs.writeFileSync('README.md', content) 41 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "extends": "@tsconfig/node20/tsconfig.json", 4 | "compilerOptions": { 5 | "module": "es2022", 6 | "moduleResolution": "bundler" 7 | } 8 | } 9 | --------------------------------------------------------------------------------