├── .git-blame-ignore-revs ├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── ci.yml │ ├── github-dependency-graph.yml │ └── release.yml ├── .gitignore ├── .mill-version ├── .scalafix.conf ├── .scalafmt.conf ├── LICENSE ├── README.md ├── build.sc ├── domain └── src │ └── io │ └── kipp │ └── github │ └── dependency │ └── graph │ └── domain │ ├── DependencyNode.scala │ ├── DependencySnapshot.scala │ ├── Detector.scala │ ├── FileInfo.scala │ ├── Job.scala │ └── Manifest.scala ├── itest └── src │ ├── cyclical │ ├── build.sc │ └── manifests.json │ ├── directRelationship │ └── build.sc │ ├── eviction │ └── build.sc │ ├── minimal │ ├── build.sc │ └── manifests.json │ ├── range │ └── build.sc │ └── reconciledRange │ └── build.sc ├── mill └── plugin ├── src-mill0.10 └── io │ └── kipp │ └── mill │ └── github │ └── dependency │ └── graph │ ├── Discover.scala │ ├── Graph.scala │ └── Resolver.scala ├── src-mill0.11 └── io │ └── kipp │ └── mill │ └── github │ └── dependency │ └── graph │ ├── Discover.scala │ ├── Graph.scala │ └── Resolver.scala ├── src-mill0.12 └── io │ └── kipp │ └── mill │ └── github │ └── dependency │ └── graph │ ├── Discover.scala │ ├── Graph.scala │ └── Resolver.scala └── src └── io └── kipp └── mill └── github └── dependency └── graph ├── Github.scala ├── GraphModule.scala ├── ModuleTrees.scala └── Writers.scala /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # Scala Steward: Reformat with scalafmt 3.8.6 2 | 4c6d0b3c42193e09d9c775dd68cb2aec2fbbbf9f 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: ckipp01 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "weekly" 8 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | paths-ignore: 9 | - 'README.md' 10 | 11 | concurrency: 12 | # Taken from scalameta/metals 13 | # On main, we don't want any jobs cancelled so the sha is used to name the group 14 | # On PR branches, we cancel the job if new commits are pushed 15 | group: ${{ (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags') ) && format('contributor-pr-base-{0}', github.sha) || format('contributor-pr-{0}', github.ref) }} 16 | cancel-in-progress: true 17 | 18 | env: 19 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 20 | 21 | jobs: 22 | style-check: 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v4 26 | - uses: coursier/cache-action@v6 27 | - uses: actions/setup-java@v4 28 | with: 29 | distribution: 'temurin' 30 | java-version: '17' 31 | 32 | - name: Check formatting 33 | run: 34 | ./mill -i __.checkFormat 35 | - name: Check scalafix 36 | run: 37 | ./mill -i __.fix --check 38 | 39 | test: 40 | runs-on: 'ubuntu-latest' 41 | strategy: 42 | fail-fast: false 43 | matrix: 44 | java: ['11', '17'] 45 | 46 | steps: 47 | - uses: actions/checkout@v4 48 | - uses: coursier/cache-action@v6 49 | - uses: actions/setup-java@v4 50 | with: 51 | distribution: 'temurin' 52 | java-version: ${{ matrix.java }} 53 | 54 | - name: Compile 55 | run: 56 | ./mill -i __.compile 57 | 58 | - name: Test 59 | run: 60 | ./mill -i --debug itest[_].test 61 | -------------------------------------------------------------------------------- /.github/workflows/github-dependency-graph.yml: -------------------------------------------------------------------------------- 1 | name: github-dependency-graph 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | submit-dependency-graph: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: coursier/cache-action@v6 14 | - uses: actions/setup-java@v4 15 | with: 16 | distribution: 'temurin' 17 | java-version: '17' 18 | - uses: ckipp01/mill-dependency-submission@v1 19 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | branches: 5 | - main 6 | tags: ["*"] 7 | jobs: 8 | publish: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | with: 13 | fetch-depth: 0 14 | - uses: actions/setup-java@v4 15 | with: 16 | distribution: 'temurin' 17 | java-version: '17' 18 | - run: ./mill -i io.kipp.mill.ci.release.ReleaseModule/publishAll 19 | env: 20 | PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} 21 | PGP_SECRET: ${{ secrets.PGP_SECRET }} 22 | SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} 23 | SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .bloop/ 2 | .bsp/ 3 | .metals/ 4 | out/ 5 | manifest.json 6 | -------------------------------------------------------------------------------- /.mill-version: -------------------------------------------------------------------------------- 1 | 0.12.4 2 | -------------------------------------------------------------------------------- /.scalafix.conf: -------------------------------------------------------------------------------- 1 | rules = [ 2 | DisableSyntax 3 | ExplicitResultTypes 4 | LeakingImplicitClassVal 5 | NoAutoTupling 6 | NoValInForComprehension 7 | OrganizeImports 8 | ProcedureSyntax 9 | ] 10 | -------------------------------------------------------------------------------- /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | version = "3.9.4" 2 | project.git = true 3 | runner.dialect = scala213 4 | docstrings.wrap = no 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2022 Chris Kipp 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mill GitHub Dependency Graph 2 | 3 | A [Mill](https://com-lihaoyi.github.io/mill/mill/Intro_to_Mill.html) plugin to 4 | submit your dependency graph to GitHub via their [Dependency Submission 5 | API](https://github.blog/2022-06-17-creating-comprehensive-dependency-graph-build-time-detection/). 6 | 7 | The main benifits of doing this are: 8 | 9 | 1. Being able to see your dependency graph on GitHub in your [Insights 10 | tab](https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/exploring-the-dependencies-of-a-repository#viewing-the-dependency-graph). 11 | For example you can see this 12 | [here](https://github.com/ckipp01/mill-github-dependency-graph/network/dependencies) 13 | for this plugin. 14 | 2. If enabled, Dependabot can send you 15 | [alerts](https://docs.github.com/en/code-security/dependabot/dependabot-alerts/viewing-and-updating-dependabot-alerts) 16 | about security vulnerabilities in your dependencies. 17 | 18 | ## Requirements 19 | 20 | - Make sure in your repo settings the Dependency Graph feature is enabled as 21 | well as Dependabot Alerts if you'd like them. (Settings -> Code security and 22 | analysis) 23 | 24 | ## Quick Start 25 | 26 | The easiest way to use this plugin is with the [mill-dependency-submission](https://github.com/ckipp01/mill-dependency-submission) action. You can add this to a workflow like below: 27 | 28 | ```yml 29 | name: github-dependency-graph 30 | 31 | on: 32 | push: 33 | branches: 34 | - main 35 | 36 | jobs: 37 | submit-dependency-graph: 38 | runs-on: ubuntu-latest 39 | steps: 40 | - uses: actions/checkout@v3 41 | - uses: coursier/cache-action@v6 42 | - uses: actions/setup-java@v3 43 | with: 44 | distribution: 'temurin' 45 | java-version: '17' 46 | - uses: ckipp01/mill-dependency-submission@v1 47 | ``` 48 | 49 | You can also just run the following command from the root of your workspace 50 | which will create the file for you: 51 | 52 | ```sh 53 | curl -o .github/workflows/github-dependency-graph.yml --create-dirs https://raw.githubusercontent.com/ckipp01/mill-github-dependency-graph/main/.github/workflows/github-dependency-graph.yml 54 | ``` 55 | After you submit your graph you'll be able to [view your 56 | dependencies](https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/exploring-the-dependencies-of-a-repository#viewing-the-dependency-graph). 57 | 58 | ## How's this work? 59 | 60 | The general idea is that the plugin works in a few steps: 61 | 62 | 1. Gather all the modules in your build 63 | 2. Gather all direct and transitive dependencies of those modules 64 | 3. Create a tree-like structure of these dependencies. We piggy back off 65 | coursier for this and use its `DependencyTree` functionality. 66 | 4. We map this structure to that of a [`DependencySnapshot`](https://github.com/ckipp01/mill-github-dependency-graph/blob/main/domain/src/io/kipp/github/dependency/graph/domain/DependencySnapshot.scala), which is what GitHub understands 67 | 5. We post this data to GitHub. 68 | 69 | You can use another available task to see what the 70 | [`Manifest`s](https://github.com/ckipp01/mill-github-dependency-graph/blob/main/domain/src/io/kipp/github/dependency/graph/domain/Manifest.scala) 71 | look like locally for your project, which are the main part of the 72 | `DependencySnapshot`. 73 | 74 | 75 | ```sh 76 | ./mill --import ivy:io.chris-kipp::mill-github-dependency-graph::0.1.0 show io.kipp.mill.github.dependency.graph.Graph/generate 77 | ``` 78 | 79 | ### Limitation 80 | 81 | You'll notice when using this that a lot of dependencies aren't linked back to 82 | the repositories where they are located, some may be wrongly linked, and much of 83 | the information the plugin is providing (like direct vs indirect) isn't actually 84 | displayed in the UI. Much of this is either bugs or limitations on the GitHub UI 85 | side. You can follow some conversation on this [here](https://github.com/orgs/community/discussions/19492). 86 | -------------------------------------------------------------------------------- /build.sc: -------------------------------------------------------------------------------- 1 | import $ivy.`com.goyeau::mill-scalafix::0.4.2` 2 | import $ivy.`com.lihaoyi::mill-contrib-buildinfo:$MILL_VERSION` 3 | import $ivy.`de.tototec::de.tobiasroeser.mill.integrationtest::0.7.1` 4 | import $ivy.`io.chris-kipp::mill-ci-release::0.1.10` 5 | 6 | import mill._ 7 | import scalalib._ 8 | import scalafmt._ 9 | import publish._ 10 | import mill.scalalib.publish._ 11 | import mill.scalalib.api.ZincWorkerUtil 12 | import mill.scalalib.api.ZincWorkerUtil._ 13 | import com.goyeau.mill.scalafix.ScalafixModule 14 | import mill.contrib.buildinfo.BuildInfo 15 | import de.tobiasroeser.mill.integrationtest._ 16 | import de.tobiasroeser.mill.vcs.version.VcsVersion 17 | import io.kipp.mill.ci.release.CiReleaseModule 18 | import io.kipp.mill.ci.release.SonatypeHost 19 | 20 | val millVersions = Seq("0.12.4", "0.11.12", "0.10.15") 21 | val millBinaryVersions = millVersions.map(scalaNativeBinaryVersion) 22 | val scala213 = "2.13.14" 23 | val artifactBase = "mill-github-dependency-graph" 24 | 25 | def millBinaryVersion(millVersion: String) = scalaNativeBinaryVersion( 26 | millVersion 27 | ) 28 | 29 | def millVersion(binaryVersion: String) = 30 | millVersions.find(v => millBinaryVersion(v) == binaryVersion).get 31 | 32 | trait Common 33 | extends ScalaModule 34 | with CiReleaseModule 35 | with ScalafixModule 36 | with ScalafmtModule { 37 | 38 | def pomSettings = PomSettings( 39 | description = "Submit your mill project's dependency graph to GitHub", 40 | organization = "io.chris-kipp", 41 | url = "https://github.com/ckipp01/mill-github-dependency-graph", 42 | licenses = Seq(License.`Apache-2.0`), 43 | versionControl = VersionControl 44 | .github(owner = "ckipp01", repo = "mill-github-dependency-graph"), 45 | developers = 46 | Seq(Developer("ckipp01", "Chris Kipp", "https://www.chris-kipp.io")) 47 | ) 48 | 49 | override def sonatypeHost: Option[SonatypeHost] = Some(SonatypeHost.s01) 50 | 51 | def scalaVersion = scala213 52 | 53 | def scalacOptions = Seq("-Ywarn-unused", "-deprecation") 54 | 55 | def scalafixScalaBinaryVersion = ZincWorkerUtil.scalaBinaryVersion(scala213) 56 | } 57 | 58 | object domain extends Common { 59 | override def artifactName = "github-dependency-graph-domain" 60 | } 61 | 62 | object plugin extends Cross[Plugin](millBinaryVersions) 63 | trait Plugin extends Cross.Module[String] with Common with BuildInfo { 64 | 65 | override def sources = T.sources { 66 | super.sources() ++ Seq( 67 | millSourcePath / s"src-mill${millVersion(crossValue).split('.').take(2).mkString(".")}" 68 | ).map(PathRef(_)) 69 | } 70 | 71 | override def artifactName = 72 | s"${artifactBase}_mill${crossValue}" 73 | 74 | override def moduleDeps = Seq(domain) 75 | override def compileIvyDeps = super.compileIvyDeps() ++ Agg( 76 | ivy"com.lihaoyi::mill-scalalib:${millVersion(crossValue)}" 77 | ) 78 | 79 | override def ivyDeps = super.ivyDeps() ++ Agg( 80 | ivy"com.lihaoyi::upickle:3.1.4", 81 | ivy"com.lihaoyi::requests:0.9.0", 82 | ivy"com.github.package-url:packageurl-java:1.5.0" 83 | ) 84 | 85 | override def buildInfoMembers = Seq( 86 | BuildInfo.Value("detectorName", artifactBase), 87 | BuildInfo.Value("homepage", pomSettings().url), 88 | BuildInfo.Value("version", publishVersion()) 89 | ) 90 | override def buildInfoObjectName = "BuildInfo" 91 | override def buildInfoPackageName = "io.kipp.mill.github.dependency.graph" 92 | } 93 | 94 | object itest extends Cross[ItestCross](millVersions) 95 | trait ItestCross extends Cross.Module[String] with MillIntegrationTestModule { 96 | 97 | def millTestVersion = crossValue 98 | 99 | def pluginsUnderTest = Seq(plugin(millBinaryVersion(crossValue))) 100 | 101 | def testBase = millSourcePath / "src" 102 | 103 | override def testInvocations: T[Seq[(PathRef, Seq[TestInvocation.Targets])]] = 104 | T { 105 | val env = if (millTestVersion() >= "0.12") 106 | Map( 107 | "COURSIER_REPOSITORIES" -> s"central sonatype:releases ivy:file://${T.dest.toString.replaceFirst("testInvocations", "test")}/ivyRepo/local/[organisation]/[module]/[revision]/[type]s/[artifact].[ext]" 108 | ) 109 | else 110 | Map.empty[String, String] 111 | Seq( 112 | "minimal" -> "checkManifest", 113 | "directRelationship" -> "verify", 114 | "eviction" -> "verify", 115 | "range" -> "verify", 116 | "reconciledRange" -> "verify", 117 | "cyclical" -> "checkManifest" 118 | ).map { case (testName, testMethod) => 119 | PathRef(testBase / testName) -> Seq( 120 | TestInvocation.Targets( 121 | Seq(testMethod), 122 | noServer = true, 123 | env = env 124 | ) 125 | ) 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /domain/src/io/kipp/github/dependency/graph/domain/DependencyNode.scala: -------------------------------------------------------------------------------- 1 | package io.kipp.github.dependency.graph.domain 2 | 3 | /** Represents a single dependency. 4 | * 5 | * @param package_url Package-url (PURL) of dependency. See 6 | * https://github.com/package-url/purl-spec for more details. 7 | * @param metadata User-defined metadata to store domain-specific information 8 | * limited to 8 keys with scalar values. 9 | * @param relationship A notation of whether a dependency is requested 10 | * directly by this manifest or is a dependency of another dependency. 11 | * @param scope A notation of whether the dependency is required for the 12 | * primary build artifact (runtime) or is only used for development. Future 13 | * versions of this specification may allow for more granular scopes. 14 | * @param dependencies Array of package-url (PURLs) of direct child dependencies. 15 | */ 16 | final case class DependencyNode( 17 | package_url: Option[String], // TODO we could make a PURL type for this 18 | metadata: Map[String, String], 19 | relationship: Option[DependencyRelationship], 20 | scope: Option[DependencyScope], 21 | dependencies: Seq[String] 22 | ) { 23 | 24 | /** Returns true if both the relationship exists and is direct 25 | */ 26 | def isDirectDependency: Boolean = relationship match { 27 | case None => false 28 | case Some(DependencyRelationship.indirect) => false 29 | case Some(DependencyRelationship.direct) => true 30 | } 31 | 32 | } 33 | 34 | /** A notation of whether a dependency is requested directly 35 | * by this manifest, or is a dependency of another dependency. 36 | */ 37 | sealed trait DependencyRelationship extends Product with Serializable 38 | object DependencyRelationship { 39 | case object direct extends DependencyRelationship 40 | case object indirect extends DependencyRelationship 41 | } 42 | 43 | /** A notation of whether the dependency is required for the primary 44 | * build artifact (runtime), or is only used for development. 45 | * Future versions of this specification may allow for more granular 46 | * scopes, like `runtime:server`, `runtime:shipped`, 47 | * `development:test`, `development:benchmark`. 48 | */ 49 | sealed trait DependencyScope extends Product with Serializable 50 | object DependencyScope { 51 | case object runtime extends DependencyScope 52 | case object development extends DependencyScope 53 | } 54 | -------------------------------------------------------------------------------- /domain/src/io/kipp/github/dependency/graph/domain/DependencySnapshot.scala: -------------------------------------------------------------------------------- 1 | package io.kipp.github.dependency.graph.domain 2 | 3 | /** Dependency submission snapshot for GitHub. 4 | * Modeled after the info found in: 5 | * https://docs.github.com/en/rest/dependency-graph/dependency-submission#create-a-snapshot-of-dependencies-for-a-repository 6 | * @param version The version of the repository snapshot submission 7 | * @param job The Job being sumbitted 8 | * @param sha The commit SHA associated with this dependency snapshot 9 | * @param ref The repository branch that triggered this snapshot 10 | * @param detector A description of the detector used. 11 | * @param metadata User-defined metadata to store domain-specific information 12 | * limited to 8 keys with scalar values 13 | * @param manifests A collection of package manifests 14 | * @param scanned The time at which the snapshot was scanned in ISO8601Date 15 | */ 16 | final case class DependencySnapshot( 17 | version: Int, 18 | job: Job, 19 | sha: String, 20 | ref: String, 21 | detector: Detector, 22 | metadata: Map[ 23 | String, 24 | String 25 | ], 26 | manifests: Map[String, Manifest], 27 | scanned: String 28 | ) 29 | -------------------------------------------------------------------------------- /domain/src/io/kipp/github/dependency/graph/domain/Detector.scala: -------------------------------------------------------------------------------- 1 | package io.kipp.github.dependency.graph.domain 2 | 3 | /** Detector is the representation of the tool used to gather the dependency 4 | * snapshot information. 5 | * 6 | * @param name The name of the detector used 7 | * @param url The url of the detector used 8 | * @param version The version of the detector used 9 | */ 10 | final case class Detector( 11 | name: String, 12 | url: String, 13 | version: String 14 | ) 15 | -------------------------------------------------------------------------------- /domain/src/io/kipp/github/dependency/graph/domain/FileInfo.scala: -------------------------------------------------------------------------------- 1 | package io.kipp.github.dependency.graph.domain 2 | 3 | /** Representing the infomation for the manifest file. 4 | * 5 | * @param source_location The path of the manifest file relative to the root 6 | * of the Git repository. 7 | */ 8 | final case class FileInfo(source_location: String) 9 | -------------------------------------------------------------------------------- /domain/src/io/kipp/github/dependency/graph/domain/Job.scala: -------------------------------------------------------------------------------- 1 | package io.kipp.github.dependency.graph.domain 2 | 3 | /** Job that is being submitted by the dependency snapshot 4 | * 5 | * @param id The external ID of the job 6 | * @param correlator Correlator provides a key that is used to group snapshots 7 | * submitted over time. Only the "latest" submitted snapshot for a given 8 | * combination of job.correlator and detector.name will be considered when 9 | * calculating a repository's current dependencies. Correlator should be as 10 | * unique as it takes to distinguish all detection runs for a given "wave" of 11 | * CI workflow you run. If you're using GitHub Actions, a good default value 12 | * for this could be the environment variables GITHUB_WORKFLOW and GITHUB_JOB 13 | * concatenated together. If you're using a build matrix, then you'll also 14 | * need to add additional key(s) to distinguish between each submission inside 15 | * a matrix variation. 16 | * @param html_url The url for the job 17 | */ 18 | final case class Job( 19 | id: String, 20 | correlator: String, 21 | html_url: Option[String] 22 | ) 23 | -------------------------------------------------------------------------------- /domain/src/io/kipp/github/dependency/graph/domain/Manifest.scala: -------------------------------------------------------------------------------- 1 | package io.kipp.github.dependency.graph.domain 2 | 3 | /** User-defined metadata to store domain-specific information limited to 8 4 | * keys with scalar values. 5 | * 6 | * @param name The name of the manifest 7 | * @param file The FileInfo 8 | * @param metadata User-defined metadata to store domain-specific information 9 | * limited to 8 keys with scalar values 10 | * @param resolved The resolved dependnecy nodes for this manifest 11 | */ 12 | final case class Manifest( 13 | name: String, 14 | file: Option[FileInfo], 15 | metadata: Map[String, String], 16 | resolved: Map[String, DependencyNode] 17 | ) 18 | -------------------------------------------------------------------------------- /itest/src/cyclical/build.sc: -------------------------------------------------------------------------------- 1 | import mill._, scalalib._ 2 | import $file.plugins 3 | import io.kipp.mill.github.dependency.graph.Graph 4 | import io.kipp.mill.github.dependency.graph.Writers._ 5 | import mill.eval.Evaluator 6 | import $ivy.`org.scalameta::munit:0.7.29` 7 | import munit.Assertions._ 8 | 9 | object overflow extends ScalaModule { 10 | 11 | def scalaVersion = "2.13.14" 12 | 13 | // See https://github.com/ckipp01/mill-github-dependency-graph/issues/77 for the context 14 | // of this test. The main issue is that when you look at the children of this dep in coursier 15 | // it is cyclical and will just continually list itself. So we have to add in some extra guards 16 | // against this. 17 | override def ivyDeps: T[Agg[Dep]] = Agg( 18 | ivy"io.netty:netty-tcnative-boringssl-static:2.0.54.Final" 19 | ) 20 | 21 | } 22 | 23 | def checkManifest(ev: Evaluator) = T.command { 24 | val projectDir = Iterator 25 | .iterate(os.pwd)(_ / os.up) 26 | .find(dir => os.exists(dir / "manifests.json")) 27 | .get 28 | val expected = ujson.read(os.read(projectDir / "manifests.json")) 29 | 30 | val manifestMapping = Graph.generate(ev)() 31 | 32 | // Lil hacky but if we compare the strings they won't match, so we read that 33 | // back up into a ujson.Value so we can compare those two 34 | val result = ujson.read(upickle.default.write(manifestMapping)) 35 | 36 | assertEquals(result, expected) 37 | } 38 | -------------------------------------------------------------------------------- /itest/src/cyclical/manifests.json: -------------------------------------------------------------------------------- 1 | { 2 | "overflow": { 3 | "name": "overflow", 4 | "metadata": { 5 | 6 | }, 7 | "resolved": { 8 | "io.netty:netty-tcnative-classes:2.0.54.Final": { 9 | "metadata": { 10 | 11 | }, 12 | "dependencies": [ 13 | 14 | ], 15 | "relationship": "indirect", 16 | "package_url": "pkg:maven/io.netty/netty-tcnative-classes@2.0.54.Final" 17 | }, 18 | "org.scala-lang:scala-library:2.13.14": { 19 | "metadata": { 20 | 21 | }, 22 | "dependencies": [ 23 | 24 | ], 25 | "relationship": "direct", 26 | "package_url": "pkg:maven/org.scala-lang/scala-library@2.13.14" 27 | }, 28 | "io.netty:netty-tcnative-boringssl-static:2.0.54.Final": { 29 | "metadata": { 30 | 31 | }, 32 | "dependencies": [ 33 | "io.netty:netty-tcnative-boringssl-static:2.0.54.Final", 34 | "io.netty:netty-tcnative-boringssl-static:2.0.54.Final", 35 | "io.netty:netty-tcnative-boringssl-static:2.0.54.Final", 36 | "io.netty:netty-tcnative-boringssl-static:2.0.54.Final", 37 | "io.netty:netty-tcnative-boringssl-static:2.0.54.Final", 38 | "io.netty:netty-tcnative-boringssl-static:2.0.54.Final", 39 | "io.netty:netty-tcnative-boringssl-static:2.0.54.Final", 40 | "io.netty:netty-tcnative-boringssl-static:2.0.54.Final", 41 | "io.netty:netty-tcnative-classes:2.0.54.Final" 42 | ], 43 | "relationship": "direct", 44 | "package_url": "pkg:maven/io.netty/netty-tcnative-boringssl-static@2.0.54.Final" 45 | } 46 | }, 47 | "file": { 48 | "source_location": "build.sc" 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /itest/src/directRelationship/build.sc: -------------------------------------------------------------------------------- 1 | import mill._, scalalib._ 2 | import $file.plugins 3 | import io.kipp.mill.github.dependency.graph.Graph 4 | import mill.eval.Evaluator 5 | import $ivy.`org.scalameta::munit:0.7.29` 6 | import munit.Assertions._ 7 | 8 | object minimal extends ScalaModule { 9 | def scalaVersion = "3.1.3" 10 | 11 | def ivyDeps = Agg( 12 | ivy"com.lihaoyi::pprint:0.7.3", 13 | ivy"com.lihaoyi::fansi:0.3.1" 14 | ) 15 | } 16 | 17 | def verify(ev: Evaluator) = T.command { 18 | val manifestMapping = Graph.generate(ev)() 19 | assert(manifestMapping.size == 1) 20 | 21 | // We want to ensure that fansi here is correctly getting marked as direct 22 | // since it will appear as a transitive of pprint being marked as indirect 23 | // first, but then should be updated when fansi is processed as a root node 24 | // to direct. Also note that it stays as direct because it's not getting 25 | // evicted. Check out the evicted test for more info. 26 | val fansiIsDirect = manifestMapping.head._2.resolved 27 | .get("com.lihaoyi:fansi_3:0.3.1") 28 | .exists(_.isDirectDependency) 29 | 30 | assert(fansiIsDirect) 31 | } 32 | -------------------------------------------------------------------------------- /itest/src/eviction/build.sc: -------------------------------------------------------------------------------- 1 | import mill._, scalalib._ 2 | import $file.plugins 3 | import io.kipp.mill.github.dependency.graph.Graph 4 | import mill.eval.Evaluator 5 | import $ivy.`org.scalameta::munit:0.7.29` 6 | import munit.Assertions._ 7 | 8 | object minimal extends ScalaModule { 9 | def scalaVersion = "3.1.3" 10 | 11 | def ivyDeps = Agg( 12 | ivy"com.lihaoyi::pprint:0.7.3", 13 | ivy"com.lihaoyi::fansi:0.3.0" 14 | ) 15 | } 16 | 17 | def verify(ev: Evaluator) = T.command { 18 | val manifestMapping = Graph.generate(ev)() 19 | assert(manifestMapping.size == 1) 20 | 21 | // Very similiar to the directRelationship test but in this case fansi 0.3.0 22 | // _shouldn't_ be marked as direct in the end result. This is because it will 23 | // end up being evicted by 0.3.1 in pprint, so we don't need to even include 24 | // it. However, it should end up being replaced by 0.3.1 and still marked as 25 | // a direct. 26 | val fansiIsDirect = manifestMapping.head._2.resolved 27 | .get("com.lihaoyi:fansi_3:0.3.1") 28 | .exists(_.isDirectDependency) 29 | 30 | assert(fansiIsDirect) 31 | } 32 | -------------------------------------------------------------------------------- /itest/src/minimal/build.sc: -------------------------------------------------------------------------------- 1 | import mill._, scalalib._ 2 | import $file.plugins 3 | import io.kipp.mill.github.dependency.graph.Graph 4 | import io.kipp.mill.github.dependency.graph.Writers._ 5 | import mill.eval.Evaluator 6 | import $ivy.`org.scalameta::munit:0.7.29` 7 | import munit.Assertions._ 8 | 9 | object minimal extends ScalaModule { 10 | def scalaVersion = "3.1.3" 11 | 12 | def ivyDeps = Agg(ivy"com.lihaoyi::pprint:0.7.3") 13 | 14 | object test extends ScalaModuleTests with TestModule.Munit { 15 | def ivyDeps = Agg(ivy"org.scalameta::munit:0.7.29") 16 | } 17 | } 18 | 19 | def checkManifest(ev: Evaluator) = T.command { 20 | val projectDir = Iterator 21 | .iterate(os.pwd)(_ / os.up) 22 | .find(dir => os.exists(dir / "manifests.json")) 23 | .get 24 | val expected = ujson.read(os.read(projectDir / "manifests.json")) 25 | 26 | val manifestMapping = Graph.generate(ev)() 27 | 28 | // Lil hacky but if we compare the strings they won't match, so we read that 29 | // back up into a ujson.Value so we can compare those two 30 | val result = ujson.read(upickle.default.write(manifestMapping)) 31 | 32 | assertEquals(result, expected) 33 | } 34 | -------------------------------------------------------------------------------- /itest/src/minimal/manifests.json: -------------------------------------------------------------------------------- 1 | { 2 | "minimal": { 3 | "name": "minimal", 4 | "metadata": { 5 | 6 | }, 7 | "resolved": { 8 | "com.lihaoyi:pprint_3:0.7.3": { 9 | "metadata": { 10 | 11 | }, 12 | "dependencies": [ 13 | "com.lihaoyi:fansi_3:0.3.1", 14 | "com.lihaoyi:sourcecode_3:0.2.8", 15 | "org.scala-lang:scala3-library_3:3.1.3" 16 | ], 17 | "relationship": "direct", 18 | "package_url": "pkg:maven/com.lihaoyi/pprint_3@0.7.3" 19 | }, 20 | "com.lihaoyi:fansi_3:0.3.1": { 21 | "metadata": { 22 | 23 | }, 24 | "dependencies": [ 25 | "com.lihaoyi:sourcecode_3:0.2.8", 26 | "org.scala-lang:scala3-library_3:3.1.3" 27 | ], 28 | "relationship": "indirect", 29 | "package_url": "pkg:maven/com.lihaoyi/fansi_3@0.3.1" 30 | }, 31 | "com.lihaoyi:sourcecode_3:0.2.8": { 32 | "metadata": { 33 | 34 | }, 35 | "dependencies": [ 36 | "org.scala-lang:scala3-library_3:3.1.3" 37 | ], 38 | "relationship": "indirect", 39 | "package_url": "pkg:maven/com.lihaoyi/sourcecode_3@0.2.8" 40 | }, 41 | "org.scala-lang:scala-library:2.13.8": { 42 | "metadata": { 43 | 44 | }, 45 | "dependencies": [ 46 | 47 | ], 48 | "relationship": "indirect", 49 | "package_url": "pkg:maven/org.scala-lang/scala-library@2.13.8" 50 | }, 51 | "org.scala-lang:scala3-library_3:3.1.3": { 52 | "metadata": { 53 | 54 | }, 55 | "dependencies": [ 56 | "org.scala-lang:scala-library:2.13.8" 57 | ], 58 | "relationship": "direct", 59 | "package_url": "pkg:maven/org.scala-lang/scala3-library_3@3.1.3" 60 | } 61 | }, 62 | "file": { 63 | "source_location": "build.sc" 64 | } 65 | }, 66 | "minimal.test": { 67 | "name": "minimal.test", 68 | "metadata": { 69 | 70 | }, 71 | "resolved": { 72 | "com.lihaoyi:pprint_3:0.7.3": { 73 | "metadata": { 74 | 75 | }, 76 | "dependencies": [ 77 | "com.lihaoyi:fansi_3:0.3.1", 78 | "com.lihaoyi:sourcecode_3:0.2.8", 79 | "org.scala-lang:scala3-library_3:3.1.3" 80 | ], 81 | "relationship": "direct", 82 | "package_url": "pkg:maven/com.lihaoyi/pprint_3@0.7.3" 83 | }, 84 | "org.scalameta:junit-interface:0.7.29": { 85 | "metadata": { 86 | 87 | }, 88 | "dependencies": [ 89 | "junit:junit:4.13.2", 90 | "org.scala-sbt:test-interface:1.0" 91 | ], 92 | "relationship": "indirect", 93 | "package_url": "pkg:maven/org.scalameta/junit-interface@0.7.29" 94 | }, 95 | "com.lihaoyi:fansi_3:0.3.1": { 96 | "metadata": { 97 | 98 | }, 99 | "dependencies": [ 100 | "com.lihaoyi:sourcecode_3:0.2.8", 101 | "org.scala-lang:scala3-library_3:3.1.3" 102 | ], 103 | "relationship": "indirect", 104 | "package_url": "pkg:maven/com.lihaoyi/fansi_3@0.3.1" 105 | }, 106 | "org.hamcrest:hamcrest-core:1.3": { 107 | "metadata": { 108 | 109 | }, 110 | "dependencies": [ 111 | 112 | ], 113 | "relationship": "indirect", 114 | "package_url": "pkg:maven/org.hamcrest/hamcrest-core@1.3" 115 | }, 116 | "org.scalameta:munit_3:0.7.29": { 117 | "metadata": { 118 | 119 | }, 120 | "dependencies": [ 121 | "junit:junit:4.13.2", 122 | "org.scala-lang:scala3-library_3:3.1.3", 123 | "org.scalameta:junit-interface:0.7.29" 124 | ], 125 | "relationship": "direct", 126 | "package_url": "pkg:maven/org.scalameta/munit_3@0.7.29" 127 | }, 128 | "org.scala-sbt:test-interface:1.0": { 129 | "metadata": { 130 | 131 | }, 132 | "dependencies": [ 133 | 134 | ], 135 | "relationship": "indirect", 136 | "package_url": "pkg:maven/org.scala-sbt/test-interface@1.0" 137 | }, 138 | "com.lihaoyi:sourcecode_3:0.2.8": { 139 | "metadata": { 140 | 141 | }, 142 | "dependencies": [ 143 | "org.scala-lang:scala3-library_3:3.1.3" 144 | ], 145 | "relationship": "indirect", 146 | "package_url": "pkg:maven/com.lihaoyi/sourcecode_3@0.2.8" 147 | }, 148 | "org.scala-lang:scala-library:2.13.8": { 149 | "metadata": { 150 | 151 | }, 152 | "dependencies": [ 153 | 154 | ], 155 | "relationship": "indirect", 156 | "package_url": "pkg:maven/org.scala-lang/scala-library@2.13.8" 157 | }, 158 | "org.scala-lang:scala3-library_3:3.1.3": { 159 | "metadata": { 160 | 161 | }, 162 | "dependencies": [ 163 | "org.scala-lang:scala-library:2.13.8" 164 | ], 165 | "relationship": "direct", 166 | "package_url": "pkg:maven/org.scala-lang/scala3-library_3@3.1.3" 167 | }, 168 | "junit:junit:4.13.2": { 169 | "metadata": { 170 | 171 | }, 172 | "dependencies": [ 173 | "org.hamcrest:hamcrest-core:1.3" 174 | ], 175 | "relationship": "indirect", 176 | "package_url": "pkg:maven/junit/junit@4.13.2" 177 | } 178 | }, 179 | "file": { 180 | "source_location": "build.sc" 181 | } 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /itest/src/range/build.sc: -------------------------------------------------------------------------------- 1 | import mill._, scalalib._ 2 | import $file.plugins 3 | import io.kipp.mill.github.dependency.graph.Graph 4 | import mill.eval.Evaluator 5 | import $ivy.`org.scalameta::munit:0.7.29` 6 | import munit.Assertions._ 7 | 8 | object minimal extends ScalaModule { 9 | // scala-steward:off 10 | def scalaVersion = "3.1.3" 11 | def ivyDeps = Agg( 12 | ivy"org.eclipse.lsp4j:org.eclipse.lsp4j.jsonrpc:0.12.0" 13 | ) 14 | } 15 | 16 | def verify(ev: Evaluator) = T.command { 17 | val manifestMapping = Graph.generate(ev)() 18 | assert(manifestMapping.size == 1) 19 | 20 | // We want to ensure that the transitive dependency of the above, which has a 21 | // range dependency doesn't end up in the actualy manifest as a range, but 22 | // the reconciled version. So we want to ensure `2.8.9` and not 23 | // `[2.8.6,2.0)`. 24 | val expected = Set( 25 | "org.scala-lang:scala-library:2.13.8", 26 | "org.scala-lang:scala3-library_3:3.1.3", 27 | "org.eclipse.lsp4j:org.eclipse.lsp4j.jsonrpc:0.12.0", 28 | "com.google.code.gson:gson:2.8.9" 29 | // scala-steward:on 30 | ) 31 | 32 | assertEquals(manifestMapping.head._2.resolved.keys, expected) 33 | } 34 | -------------------------------------------------------------------------------- /itest/src/reconciledRange/build.sc: -------------------------------------------------------------------------------- 1 | import mill._, scalalib._ 2 | import $file.plugins 3 | import io.kipp.mill.github.dependency.graph.Graph 4 | import mill.eval.Evaluator 5 | import $ivy.`org.scalameta::munit:0.7.29` 6 | import munit.Assertions._ 7 | 8 | object minimalDep extends ScalaModule { 9 | // scala-steward:off 10 | def scalaVersion = "2.13.8" 11 | 12 | def ivyDeps = Agg( 13 | ivy"com.fasterxml.jackson.core:jackson-core:2.12.3" 14 | ) 15 | } 16 | 17 | object minimal extends ScalaModule { 18 | def moduleDeps = Seq(`minimalDep`) 19 | 20 | def scalaVersion = "2.13.8" 21 | 22 | def ivyDeps = Agg( 23 | ivy"org.jongo:jongo:1.5.0" 24 | ) 25 | } 26 | 27 | def verify(ev: Evaluator) = T.command { 28 | val manifestMapping = Graph.generate(ev)() 29 | assert(manifestMapping.size == 2) 30 | 31 | // OK, this is a super weird one. Jongo here has a range dep which is fine, and when 32 | // it's resolved by itself, it correctly gets 2.12.3 like you can see below: 33 | // 34 | // ❯ cs resolve org.jongo:jongo:1.5.0 35 | // com.fasterxml.jackson.core:jackson-annotations:2.12.3:default 36 | // com.fasterxml.jackson.core:jackson-core:2.12.3:default 37 | // com.fasterxml.jackson.core:jackson-databind:2.12.3:default 38 | // de.undercouch:bson4jackson:2.12.0:default 39 | // org.jongo:jongo:1.5.0:default 40 | // 41 | // The issue enteres with a setup like the above. For some reason coursier 42 | // will actually retain the range as the reconciledVersion in the 43 | // DependencyTree when it should be the actual reconciled versions. 44 | // 45 | // dep version: [2.7.0,2.12.3] 46 | // retained version: [2.7.0,2.12.3] 47 | // reconciled version: [2.7.0,2.12.3] 48 | // 49 | // Since I believe this to be a bug in coursier for now we'll just throw them 50 | // out to ensure we're not creating invalid PURLs. 51 | val expected = Set( 52 | "org.scala-lang:scala-library:2.13.8", 53 | "com.fasterxml.jackson.core:jackson-core:2.12.3", 54 | // NOTICE that com.fasterxml.jackson.core:jackson-core:[2.7.0,2.12.3] is not here 55 | "com.fasterxml.jackson.core:jackson-core:2.12.3", 56 | "com.fasterxml.jackson.core:jackson-databind:2.12.3", 57 | "com.fasterxml.jackson.core:jackson-annotations:2.12.3", 58 | "org.jongo:jongo:1.5.0", 59 | "de.undercouch:bson4jackson:2.12.0" 60 | // scala-steward:on 61 | ) 62 | 63 | val results = manifestMapping.foldLeft[Set[String]](Set.empty) { 64 | case (set, mapping) => 65 | set ++ mapping._2.resolved.keySet 66 | } 67 | 68 | assertEquals(results, expected) 69 | } 70 | -------------------------------------------------------------------------------- /mill: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # This is a wrapper script, that automatically selects or downloads Mill from Maven Central or GitHub release pages. 4 | # 5 | # This script determines the Mill version to use by trying these sources 6 | # - env-variable `MILL_VERSION` 7 | # - local file `.mill-version` 8 | # - local file `.config/mill-version` 9 | # - if accessible, find the latest stable version available on Maven Central (https://repo1.maven.org/maven2) 10 | # - env-variable `DEFAULT_MILL_VERSION` 11 | # 12 | # If a version has the suffix '-native' a native binary will be used. 13 | # If a version has the suffix '-jvm' an executable jar file will be used, requiring an already installed Java runtime. 14 | # If no such suffix is found, the script will pick a default based on version and platform. 15 | # 16 | # Once a version was determined, it tries to use either 17 | # - a system-installed mill, if found and it's version matches 18 | # - an already downloaded version under ~/.cache/mill/download 19 | # 20 | # If no working mill version was found on the system, 21 | # this script downloads a binary file from Maven Central or Github Pages (this is version dependent) 22 | # into a cache location (~/.cache/mill/download). 23 | # 24 | # Mill Project URL: https://github.com/com-lihaoyi/mill 25 | # Script Version: 0.13.0-M1-13-935205 26 | # 27 | # If you want to improve this script, please also contribute your changes back! 28 | # This script was generated from: scripts/src/mill.sh 29 | # 30 | # Licensed under the Apache License, Version 2.0 31 | 32 | set -e 33 | 34 | if [ -z "${DEFAULT_MILL_VERSION}" ] ; then 35 | DEFAULT_MILL_VERSION="0.12.10" 36 | fi 37 | 38 | 39 | if [ -z "${GITHUB_RELEASE_CDN}" ] ; then 40 | GITHUB_RELEASE_CDN="" 41 | fi 42 | 43 | 44 | MILL_REPO_URL="https://github.com/com-lihaoyi/mill" 45 | 46 | if [ -z "${CURL_CMD}" ] ; then 47 | CURL_CMD=curl 48 | fi 49 | 50 | # Explicit commandline argument takes precedence over all other methods 51 | if [ "$1" = "--mill-version" ] ; then 52 | shift 53 | if [ "x$1" != "x" ] ; then 54 | MILL_VERSION="$1" 55 | shift 56 | else 57 | echo "You specified --mill-version without a version." 1>&2 58 | echo "Please provide a version that matches one provided on" 1>&2 59 | echo "${MILL_REPO_URL}/releases" 1>&2 60 | false 61 | fi 62 | fi 63 | 64 | # Please note, that if a MILL_VERSION is already set in the environment, 65 | # We reuse it's value and skip searching for a value. 66 | 67 | # If not already set, read .mill-version file 68 | if [ -z "${MILL_VERSION}" ] ; then 69 | if [ -f ".mill-version" ] ; then 70 | MILL_VERSION="$(tr '\r' '\n' < .mill-version | head -n 1 2> /dev/null)" 71 | elif [ -f ".config/mill-version" ] ; then 72 | MILL_VERSION="$(tr '\r' '\n' < .config/mill-version | head -n 1 2> /dev/null)" 73 | fi 74 | fi 75 | 76 | MILL_USER_CACHE_DIR="${XDG_CACHE_HOME:-${HOME}/.cache}/mill" 77 | 78 | if [ -z "${MILL_DOWNLOAD_PATH}" ] ; then 79 | MILL_DOWNLOAD_PATH="${MILL_USER_CACHE_DIR}/download" 80 | fi 81 | 82 | # If not already set, try to fetch newest from Github 83 | if [ -z "${MILL_VERSION}" ] ; then 84 | # TODO: try to load latest version from release page 85 | echo "No mill version specified." 1>&2 86 | echo "You should provide a version via '.mill-version' file or --mill-version option." 1>&2 87 | 88 | mkdir -p "${MILL_DOWNLOAD_PATH}" 89 | LANG=C touch -d '1 hour ago' "${MILL_DOWNLOAD_PATH}/.expire_latest" 2>/dev/null || ( 90 | # we might be on OSX or BSD which don't have -d option for touch 91 | # but probably a -A [-][[hh]mm]SS 92 | touch "${MILL_DOWNLOAD_PATH}/.expire_latest"; touch -A -010000 "${MILL_DOWNLOAD_PATH}/.expire_latest" 93 | ) || ( 94 | # in case we still failed, we retry the first touch command with the intention 95 | # to show the (previously suppressed) error message 96 | LANG=C touch -d '1 hour ago' "${MILL_DOWNLOAD_PATH}/.expire_latest" 97 | ) 98 | 99 | # POSIX shell variant of bash's -nt operator, see https://unix.stackexchange.com/a/449744/6993 100 | # if [ "${MILL_DOWNLOAD_PATH}/.latest" -nt "${MILL_DOWNLOAD_PATH}/.expire_latest" ] ; then 101 | if [ -n "$(find -L "${MILL_DOWNLOAD_PATH}/.latest" -prune -newer "${MILL_DOWNLOAD_PATH}/.expire_latest")" ]; then 102 | # we know a current latest version 103 | MILL_VERSION=$(head -n 1 "${MILL_DOWNLOAD_PATH}"/.latest 2> /dev/null) 104 | fi 105 | 106 | if [ -z "${MILL_VERSION}" ] ; then 107 | # we don't know a current latest version 108 | echo "Retrieving latest mill version ..." 1>&2 109 | LANG=C ${CURL_CMD} -s -i -f -I ${MILL_REPO_URL}/releases/latest 2> /dev/null | grep --ignore-case Location: | sed s'/^.*tag\///' | tr -d '\r\n' > "${MILL_DOWNLOAD_PATH}/.latest" 110 | MILL_VERSION=$(head -n 1 "${MILL_DOWNLOAD_PATH}"/.latest 2> /dev/null) 111 | fi 112 | 113 | if [ -z "${MILL_VERSION}" ] ; then 114 | # Last resort 115 | MILL_VERSION="${DEFAULT_MILL_VERSION}" 116 | echo "Falling back to hardcoded mill version ${MILL_VERSION}" 1>&2 117 | else 118 | echo "Using mill version ${MILL_VERSION}" 1>&2 119 | fi 120 | fi 121 | 122 | MILL_NATIVE_SUFFIX="-native" 123 | MILL_JVM_SUFFIX="-jvm" 124 | FULL_MILL_VERSION=$MILL_VERSION 125 | ARTIFACT_SUFFIX="" 126 | set_artifact_suffix(){ 127 | if [ "$(expr substr $(uname -s) 1 5 2>/dev/null)" = "Linux" ]; then 128 | if [ "$(uname -m)" = "aarch64" ]; then 129 | ARTIFACT_SUFFIX="-native-linux-aarch64" 130 | else 131 | ARTIFACT_SUFFIX="-native-linux-amd64" 132 | fi 133 | elif [ "$(uname)" = "Darwin" ]; then 134 | if [ "$(uname -m)" = "arm64" ]; then 135 | ARTIFACT_SUFFIX="-native-mac-aarch64" 136 | else 137 | ARTIFACT_SUFFIX="-native-mac-amd64" 138 | fi 139 | else 140 | echo "This native mill launcher supports only Linux and macOS." 1>&2 141 | exit 1 142 | fi 143 | } 144 | 145 | case "$MILL_VERSION" in 146 | *"$MILL_NATIVE_SUFFIX") 147 | MILL_VERSION=${MILL_VERSION%"$MILL_NATIVE_SUFFIX"} 148 | set_artifact_suffix 149 | ;; 150 | 151 | *"$MILL_JVM_SUFFIX") 152 | MILL_VERSION=${MILL_VERSION%"$MILL_JVM_SUFFIX"} 153 | ;; 154 | 155 | *) 156 | case "$MILL_VERSION" in 157 | 0.1.*) ;; 158 | 0.2.*) ;; 159 | 0.3.*) ;; 160 | 0.4.*) ;; 161 | 0.5.*) ;; 162 | 0.6.*) ;; 163 | 0.7.*) ;; 164 | 0.8.*) ;; 165 | 0.9.*) ;; 166 | 0.10.*) ;; 167 | 0.11.*) ;; 168 | 0.12.*) ;; 169 | *) 170 | set_artifact_suffix 171 | esac 172 | ;; 173 | esac 174 | 175 | MILL="${MILL_DOWNLOAD_PATH}/$MILL_VERSION$ARTIFACT_SUFFIX" 176 | 177 | try_to_use_system_mill() { 178 | if [ "$(uname)" != "Linux" ]; then 179 | return 0 180 | fi 181 | 182 | MILL_IN_PATH="$(command -v mill || true)" 183 | 184 | if [ -z "${MILL_IN_PATH}" ]; then 185 | return 0 186 | fi 187 | 188 | SYSTEM_MILL_FIRST_TWO_BYTES=$(head --bytes=2 "${MILL_IN_PATH}") 189 | if [ "${SYSTEM_MILL_FIRST_TWO_BYTES}" = "#!" ]; then 190 | # MILL_IN_PATH is (very likely) a shell script and not the mill 191 | # executable, ignore it. 192 | return 0 193 | fi 194 | 195 | SYSTEM_MILL_PATH=$(readlink -e "${MILL_IN_PATH}") 196 | SYSTEM_MILL_SIZE=$(stat --format=%s "${SYSTEM_MILL_PATH}") 197 | SYSTEM_MILL_MTIME=$(stat --format=%y "${SYSTEM_MILL_PATH}") 198 | 199 | if [ ! -d "${MILL_USER_CACHE_DIR}" ]; then 200 | mkdir -p "${MILL_USER_CACHE_DIR}" 201 | fi 202 | 203 | SYSTEM_MILL_INFO_FILE="${MILL_USER_CACHE_DIR}/system-mill-info" 204 | if [ -f "${SYSTEM_MILL_INFO_FILE}" ]; then 205 | parseSystemMillInfo() { 206 | LINE_NUMBER="${1}" 207 | # Select the line number of the SYSTEM_MILL_INFO_FILE, cut the 208 | # variable definition in that line in two halves and return 209 | # the value, and finally remove the quotes. 210 | sed -n "${LINE_NUMBER}p" "${SYSTEM_MILL_INFO_FILE}" |\ 211 | cut -d= -f2 |\ 212 | sed 's/"\(.*\)"/\1/' 213 | } 214 | 215 | CACHED_SYSTEM_MILL_PATH=$(parseSystemMillInfo 1) 216 | CACHED_SYSTEM_MILL_VERSION=$(parseSystemMillInfo 2) 217 | CACHED_SYSTEM_MILL_SIZE=$(parseSystemMillInfo 3) 218 | CACHED_SYSTEM_MILL_MTIME=$(parseSystemMillInfo 4) 219 | 220 | if [ "${SYSTEM_MILL_PATH}" = "${CACHED_SYSTEM_MILL_PATH}" ] \ 221 | && [ "${SYSTEM_MILL_SIZE}" = "${CACHED_SYSTEM_MILL_SIZE}" ] \ 222 | && [ "${SYSTEM_MILL_MTIME}" = "${CACHED_SYSTEM_MILL_MTIME}" ]; then 223 | if [ "${CACHED_SYSTEM_MILL_VERSION}" = "${MILL_VERSION}" ]; then 224 | MILL="${SYSTEM_MILL_PATH}" 225 | return 0 226 | else 227 | return 0 228 | fi 229 | fi 230 | fi 231 | 232 | SYSTEM_MILL_VERSION=$(${SYSTEM_MILL_PATH} --version | head -n1 | sed -n 's/^Mill.*version \(.*\)/\1/p') 233 | 234 | cat < "${SYSTEM_MILL_INFO_FILE}" 235 | CACHED_SYSTEM_MILL_PATH="${SYSTEM_MILL_PATH}" 236 | CACHED_SYSTEM_MILL_VERSION="${SYSTEM_MILL_VERSION}" 237 | CACHED_SYSTEM_MILL_SIZE="${SYSTEM_MILL_SIZE}" 238 | CACHED_SYSTEM_MILL_MTIME="${SYSTEM_MILL_MTIME}" 239 | EOF 240 | 241 | if [ "${SYSTEM_MILL_VERSION}" = "${MILL_VERSION}" ]; then 242 | MILL="${SYSTEM_MILL_PATH}" 243 | fi 244 | } 245 | try_to_use_system_mill 246 | 247 | # If not already downloaded, download it 248 | if [ ! -s "${MILL}" ] ; then 249 | 250 | # support old non-XDG download dir 251 | MILL_OLD_DOWNLOAD_PATH="${HOME}/.mill/download" 252 | OLD_MILL="${MILL_OLD_DOWNLOAD_PATH}/${MILL_VERSION}" 253 | if [ -x "${OLD_MILL}" ] ; then 254 | MILL="${OLD_MILL}" 255 | else 256 | case $MILL_VERSION in 257 | 0.0.* | 0.1.* | 0.2.* | 0.3.* | 0.4.* ) 258 | DOWNLOAD_SUFFIX="" 259 | DOWNLOAD_FROM_MAVEN=0 260 | ;; 261 | 0.5.* | 0.6.* | 0.7.* | 0.8.* | 0.9.* | 0.10.* | 0.11.0-M* ) 262 | DOWNLOAD_SUFFIX="-assembly" 263 | DOWNLOAD_FROM_MAVEN=0 264 | ;; 265 | *) 266 | DOWNLOAD_SUFFIX="-assembly" 267 | DOWNLOAD_FROM_MAVEN=1 268 | ;; 269 | esac 270 | 271 | DOWNLOAD_FILE=$(mktemp mill.XXXXXX) 272 | 273 | if [ "$DOWNLOAD_FROM_MAVEN" = "1" ] ; then 274 | DOWNLOAD_URL="https://repo1.maven.org/maven2/com/lihaoyi/mill-dist${ARTIFACT_SUFFIX}/${MILL_VERSION}/mill-dist${ARTIFACT_SUFFIX}-${MILL_VERSION}.jar" 275 | else 276 | MILL_VERSION_TAG=$(echo "$MILL_VERSION" | sed -E 's/([^-]+)(-M[0-9]+)?(-.*)?/\1\2/') 277 | DOWNLOAD_URL="${GITHUB_RELEASE_CDN}${MILL_REPO_URL}/releases/download/${MILL_VERSION_TAG}/${MILL_VERSION}${DOWNLOAD_SUFFIX}" 278 | unset MILL_VERSION_TAG 279 | fi 280 | 281 | # TODO: handle command not found 282 | echo "Downloading mill ${MILL_VERSION} from ${DOWNLOAD_URL} ..." 1>&2 283 | ${CURL_CMD} -f -L -o "${DOWNLOAD_FILE}" "${DOWNLOAD_URL}" 284 | chmod +x "${DOWNLOAD_FILE}" 285 | mkdir -p "${MILL_DOWNLOAD_PATH}" 286 | mv "${DOWNLOAD_FILE}" "${MILL}" 287 | 288 | unset DOWNLOAD_FILE 289 | unset DOWNLOAD_SUFFIX 290 | fi 291 | fi 292 | 293 | if [ -z "$MILL_MAIN_CLI" ] ; then 294 | MILL_MAIN_CLI="${0}" 295 | fi 296 | 297 | MILL_FIRST_ARG="" 298 | if [ "$1" = "--bsp" ] || [ "$1" = "-i" ] || [ "$1" = "--interactive" ] || [ "$1" = "--no-server" ] || [ "$1" = "--repl" ] || [ "$1" = "--help" ] ; then 299 | # Need to preserve the first position of those listed options 300 | MILL_FIRST_ARG=$1 301 | shift 302 | fi 303 | 304 | unset MILL_DOWNLOAD_PATH 305 | unset MILL_OLD_DOWNLOAD_PATH 306 | unset OLD_MILL 307 | unset MILL_VERSION 308 | unset MILL_REPO_URL 309 | 310 | # We don't quote MILL_FIRST_ARG on purpose, so we can expand the empty value without quotes 311 | # shellcheck disable=SC2086 312 | exec "${MILL}" $MILL_FIRST_ARG -D "mill.main.cli=${MILL_MAIN_CLI}" "$@" 313 | -------------------------------------------------------------------------------- /plugin/src-mill0.10/io/kipp/mill/github/dependency/graph/Discover.scala: -------------------------------------------------------------------------------- 1 | package io.kipp.mill.github.dependency.graph 2 | 3 | import mill.main.EvaluatorScopt 4 | 5 | private[graph] object Discover { 6 | implicit def millScoptEvaluatorReads[A]: EvaluatorScopt[A] = 7 | new EvaluatorScopt[A]() 8 | } 9 | -------------------------------------------------------------------------------- /plugin/src-mill0.10/io/kipp/mill/github/dependency/graph/Graph.scala: -------------------------------------------------------------------------------- 1 | package io.kipp.mill.github.dependency.graph 2 | 3 | object Graph extends GraphModule { 4 | 5 | import Discover._ 6 | lazy val millDiscover: mill.define.Discover[this.type] = 7 | mill.define.Discover[this.type] 8 | } 9 | -------------------------------------------------------------------------------- /plugin/src-mill0.10/io/kipp/mill/github/dependency/graph/Resolver.scala: -------------------------------------------------------------------------------- 1 | package io.kipp.mill.github.dependency.graph 2 | 3 | import coursier.graph.DependencyTree 4 | import mill._ 5 | import mill.eval.Evaluator 6 | import mill.scalalib.Dep 7 | import mill.scalalib.JavaModule 8 | import mill.scalalib.Lib 9 | 10 | /** Utils to help find all your modules and resolve their dependencies. 11 | */ 12 | object Resolver { 13 | 14 | /** Given an evaluator and your javaModules, use coursier to resolve all of 15 | * their dependencies into trees. 16 | * 17 | * @param evaluator Evaluator passed in from the command 18 | * @param javaModules All the JavaModules to resolve dependencies from 19 | * @return A collection of ModuleTrees 20 | */ 21 | private[graph] def resolveModuleTrees( 22 | evaluator: Evaluator, 23 | javaModules: Seq[JavaModule] 24 | ): Seq[ModuleTrees] = evaluator.evalOrThrow() { 25 | javaModules.map { javaModule => 26 | T.task { 27 | 28 | val depToDependency = javaModule.resolveCoursierDependency() 29 | val deps: Agg[Dep] = 30 | javaModule.transitiveCompileIvyDeps() ++ javaModule 31 | .transitiveIvyDeps() 32 | val repos = javaModule.repositoriesTask() 33 | val mapDeps = javaModule.mapDependencies() 34 | val custom = javaModule.resolutionCustomizer() 35 | 36 | val (dependencies, resolution) = 37 | Lib.resolveDependenciesMetadata( 38 | repositories = repos, 39 | depToDependency = depToDependency, 40 | deps = deps, 41 | mapDependencies = Some(mapDeps), 42 | customizer = custom, 43 | coursierCacheCustomizer = None, 44 | ctx = Some(T.log) 45 | ) 46 | 47 | val trees = 48 | DependencyTree(resolution = resolution, roots = dependencies) 49 | 50 | ModuleTrees( 51 | javaModule, 52 | trees 53 | ) 54 | } 55 | } 56 | } 57 | 58 | private[graph] def computeModules(ev: Evaluator) = 59 | ev.rootModule.millInternal.modules.collect { case j: JavaModule => j } 60 | } 61 | -------------------------------------------------------------------------------- /plugin/src-mill0.11/io/kipp/mill/github/dependency/graph/Discover.scala: -------------------------------------------------------------------------------- 1 | package io.kipp.mill.github.dependency.graph 2 | 3 | private[graph] object Discover { 4 | implicit def millEvaluatorTokenReader 5 | : mainargs.TokensReader[mill.eval.Evaluator] = 6 | mill.main.TokenReaders.millEvaluatorTokenReader 7 | } 8 | -------------------------------------------------------------------------------- /plugin/src-mill0.11/io/kipp/mill/github/dependency/graph/Graph.scala: -------------------------------------------------------------------------------- 1 | package io.kipp.mill.github.dependency.graph 2 | 3 | import scala.annotation.nowarn 4 | 5 | // In here for the Discover import 6 | @nowarn("msg=Unused import") 7 | object Graph extends GraphModule { 8 | 9 | import Discover._ 10 | lazy val millDiscover: mill.define.Discover[this.type] = 11 | mill.define.Discover[this.type] 12 | } 13 | -------------------------------------------------------------------------------- /plugin/src-mill0.11/io/kipp/mill/github/dependency/graph/Resolver.scala: -------------------------------------------------------------------------------- 1 | package io.kipp.mill.github.dependency.graph 2 | 3 | import coursier.graph.DependencyTree 4 | import mill._ 5 | import mill.eval.Evaluator 6 | import mill.scalalib.JavaModule 7 | import mill.scalalib.Lib 8 | 9 | /** Utils to help find all your modules and resolve their dependencies. 10 | */ 11 | object Resolver { 12 | 13 | /** Given an evaluator and your javaModules, use coursier to resolve all of 14 | * their dependencies into trees. 15 | * 16 | * @param evaluator Evaluator passed in from the command 17 | * @param javaModules All the JavaModules to resolve dependencies from 18 | * @return A collection of ModuleTrees 19 | */ 20 | private[graph] def resolveModuleTrees( 21 | evaluator: Evaluator, 22 | javaModules: Seq[JavaModule] 23 | ): Seq[ModuleTrees] = evaluator.evalOrThrow() { 24 | javaModules.map { javaModule => 25 | T.task { 26 | 27 | val deps = 28 | javaModule.transitiveCompileIvyDeps() ++ javaModule 29 | .transitiveIvyDeps() 30 | val repos = javaModule.repositoriesTask() 31 | val mapDeps = javaModule.mapDependencies() 32 | val custom = javaModule.resolutionCustomizer() 33 | 34 | val (dependencies, resolution) = 35 | Lib.resolveDependenciesMetadata( 36 | repositories = repos, 37 | deps = deps, 38 | mapDependencies = Some(mapDeps), 39 | customizer = custom, 40 | ctx = Some(T.log) 41 | ) 42 | 43 | val trees = 44 | DependencyTree(resolution = resolution, roots = dependencies) 45 | 46 | ModuleTrees( 47 | javaModule, 48 | trees 49 | ) 50 | } 51 | } 52 | } 53 | 54 | private[graph] def computeModules(ev: Evaluator) = 55 | ev.rootModule.millInternal.modules.collect { case j: JavaModule => j } 56 | } 57 | -------------------------------------------------------------------------------- /plugin/src-mill0.12/io/kipp/mill/github/dependency/graph/Discover.scala: -------------------------------------------------------------------------------- 1 | package io.kipp.mill.github.dependency.graph 2 | 3 | private[graph] object Discover { 4 | implicit def millEvaluatorTokenReader 5 | : mainargs.TokensReader[mill.eval.Evaluator] = 6 | mill.main.TokenReaders.millEvaluatorTokenReader 7 | } 8 | -------------------------------------------------------------------------------- /plugin/src-mill0.12/io/kipp/mill/github/dependency/graph/Graph.scala: -------------------------------------------------------------------------------- 1 | package io.kipp.mill.github.dependency.graph 2 | 3 | object Graph extends GraphModule { 4 | 5 | import Discover._ 6 | lazy val millDiscover: mill.define.Discover = 7 | mill.define.Discover[this.type] 8 | } 9 | -------------------------------------------------------------------------------- /plugin/src-mill0.12/io/kipp/mill/github/dependency/graph/Resolver.scala: -------------------------------------------------------------------------------- 1 | package io.kipp.mill.github.dependency.graph 2 | 3 | import coursier.graph.DependencyTree 4 | import mill._ 5 | import mill.eval.Evaluator 6 | import mill.scalalib.JavaModule 7 | import mill.scalalib.Lib 8 | 9 | /** Utils to help find all your modules and resolve their dependencies. 10 | */ 11 | object Resolver { 12 | 13 | /** Given an evaluator and your javaModules, use coursier to resolve all of 14 | * their dependencies into trees. 15 | * 16 | * @param evaluator Evaluator passed in from the command 17 | * @param javaModules All the JavaModules to resolve dependencies from 18 | * @return A collection of ModuleTrees 19 | */ 20 | private[graph] def resolveModuleTrees( 21 | evaluator: Evaluator, 22 | javaModules: Seq[JavaModule] 23 | ): Seq[ModuleTrees] = evaluator.evalOrThrow() { 24 | javaModules.map { javaModule => 25 | Task.Anon { 26 | 27 | val deps = 28 | javaModule.transitiveCompileIvyDeps() ++ javaModule 29 | .transitiveIvyDeps() 30 | val repos = javaModule.repositoriesTask() 31 | val mapDeps = javaModule.mapDependencies() 32 | val custom = javaModule.resolutionCustomizer() 33 | 34 | Lib 35 | .resolveDependenciesMetadataSafe( 36 | repositories = repos, 37 | deps = deps, 38 | mapDependencies = Some(mapDeps), 39 | customizer = custom, 40 | ctx = Some(T.log) 41 | ) 42 | .map { resolution => 43 | val trees = 44 | DependencyTree( 45 | resolution = resolution, 46 | roots = deps.map(_.dep).toSeq 47 | ) 48 | 49 | ModuleTrees( 50 | javaModule, 51 | trees 52 | ) 53 | 54 | } 55 | 56 | } 57 | } 58 | } 59 | 60 | private[graph] def computeModules(ev: Evaluator) = 61 | ev.rootModule.millInternal.modules.collect { case j: JavaModule => j } 62 | } 63 | -------------------------------------------------------------------------------- /plugin/src/io/kipp/mill/github/dependency/graph/Github.scala: -------------------------------------------------------------------------------- 1 | package io.kipp.mill.github.dependency.graph 2 | 3 | import io.kipp.github.dependency.graph.domain.DependencySnapshot 4 | import io.kipp.github.dependency.graph.domain.Detector 5 | import io.kipp.github.dependency.graph.domain.Job 6 | import io.kipp.github.dependency.graph.domain.Manifest 7 | 8 | import java.net.URI 9 | import java.time.Instant 10 | import scala.util.Properties 11 | 12 | import Writers._ 13 | 14 | /** Handles all operation of dealing with the environement when running in a 15 | * GitHub action and also submitting the snapshot to GitHub. 16 | */ 17 | object Github { 18 | 19 | private val url = new URI( 20 | s"${Env.githubApiUrl}/repos/${Env.githubRepository}/dependency-graph/snapshots" 21 | ) 22 | 23 | /** Does the actual submission to the GitHub API. 24 | * 25 | * @param snapshot The full snapshot to submit. 26 | * @param ctx 27 | */ 28 | def submit(snapshot: DependencySnapshot)(implicit ctx: mill.api.Ctx): Unit = { 29 | ctx.log.info("Submitting your snapshot to GitHub...") 30 | val payload = upickle.default.write(snapshot) 31 | val result = requests.post( 32 | url.toString, 33 | headers = Map( 34 | "Content-Type" -> "application/json", 35 | "Authorization" -> s"token ${Env.githubToken}" 36 | ), 37 | data = payload, 38 | check = false 39 | ) 40 | 41 | if (result.is2xx) { 42 | val manifestModules = snapshot.manifests.size 43 | val totalDependencies = 44 | snapshot.manifests.values.foldLeft[Seq[String]](Seq.empty) { 45 | (total, manifest) => 46 | total ++ manifest.resolved.keys 47 | } 48 | val totalSize = totalDependencies.size 49 | val uniqueSize = totalDependencies.toSet.size 50 | 51 | ctx.log.info(s"""Correctly submitted your snapshot to GitHub! 52 | | 53 | |Here are some fun stats! 54 | | 55 | |We submitted dependencies for ${manifestModules} modules. 56 | |This was a total of ${totalSize} dependencies. 57 | |${uniqueSize} were unique. 58 | |""".stripMargin) 59 | } else if (result.statusCode == 404) { 60 | val msg = 61 | """Encountered a 404, make sure you have "Dependency Graph" enabled under "Settings -> Code Security and analysis"""" 62 | throw new Exception(msg) 63 | } else if (result.statusCode == 401) { 64 | val msg = """Unable to correctly authenticate with GitHub. 65 | | 66 | |Make sure you have the correct github token set up in your env.""".stripMargin 67 | throw new Exception(msg) 68 | } else { 69 | val msg = 70 | s"""It looks like something went wrong when trying to submit your dependency graph. 71 | | 72 | |[${result.statusCode}] ${result.data}""".stripMargin 73 | throw new Exception(msg) 74 | } 75 | } 76 | 77 | /** Given manifests for the project, create a full snapshot with them. 78 | * 79 | * @param manifests All of the manifests for the project 80 | * @return The full DependencySnapshot to be submitted to GitHub 81 | */ 82 | def snapshot(manifests: Map[String, Manifest]): DependencySnapshot = 83 | DependencySnapshot( 84 | // TODO how do we increment this? Do we query the api for the last version? 85 | version = 0, 86 | job = githubJob, 87 | sha = Env.githubSha, 88 | ref = Env.githubRef, 89 | detector = detector, 90 | metadata = Map.empty, 91 | manifests = manifests, 92 | scanned = Instant.now().toString() 93 | ) 94 | 95 | private lazy val detector = Detector( 96 | BuildInfo.detectorName, 97 | BuildInfo.homepage, 98 | BuildInfo.version 99 | ) 100 | 101 | private lazy val githubJob: Job = { 102 | val correlator = s"${Env.githubJobName}_${Env.githubWorkflow}" 103 | val id = Env.githubRunId 104 | val html_url = 105 | for { 106 | serverUrl <- Properties.envOrNone("$GITHUB_SERVER_URL") 107 | repository <- Properties.envOrNone("GITHUB_REPOSITORY") 108 | } yield s"$serverUrl/$repository/actions/runs/$id" 109 | Job(id = id, correlator = correlator, html_url = html_url) 110 | } 111 | 112 | object Env { 113 | lazy val githubWorkflow: String = githubCIEnv("GITHUB_WORKFLOW") 114 | lazy val githubJobName: String = githubCIEnv("GITHUB_JOB") 115 | lazy val githubRunId: String = githubCIEnv("GITHUB_RUN_ID") 116 | lazy val githubSha: String = githubCIEnv("GITHUB_SHA") 117 | lazy val githubRef: String = githubCIEnv("GITHUB_REF") 118 | lazy val githubApiUrl: String = githubCIEnv("GITHUB_API_URL") 119 | lazy val githubRepository: String = githubCIEnv("GITHUB_REPOSITORY") 120 | lazy val githubToken: String = githubCIEnv("GITHUB_TOKEN") 121 | 122 | private def githubCIEnv( 123 | name: String 124 | ): String = 125 | Properties.envOrNone(name).getOrElse { 126 | val msg = s"""|It looks like there is no "${name}" set as an env variable. 127 | | 128 | |Are you sure you're in a GitHubAction? 129 | | 130 | |If you're testing locally try to call "generate" instead of "submit" . 131 | """.stripMargin 132 | throw new Exception(msg) 133 | } 134 | 135 | } 136 | 137 | } 138 | -------------------------------------------------------------------------------- /plugin/src/io/kipp/mill/github/dependency/graph/GraphModule.scala: -------------------------------------------------------------------------------- 1 | package io.kipp.mill.github.dependency.graph 2 | 3 | import io.kipp.github.dependency.graph.domain 4 | import mill._ 5 | import mill.define.Command 6 | import mill.define.ExternalModule 7 | import mill.eval.Evaluator 8 | 9 | trait GraphModule extends ExternalModule { 10 | 11 | import Writers._ 12 | 13 | def submit(ev: Evaluator): Command[Unit] = T.command { 14 | val manifests = generate(ev)() 15 | val snapshot = Github.snapshot(manifests) 16 | Github.submit(snapshot) 17 | } 18 | 19 | def generate(ev: Evaluator): Command[Map[String, domain.Manifest]] = 20 | T.command { 21 | val modules = Resolver.computeModules(ev) 22 | val moduleTrees = Resolver.resolveModuleTrees(ev, modules) 23 | val manifests: Map[String, domain.Manifest] = 24 | moduleTrees.map(mt => (mt.module.toString(), mt.toManifest())).toMap 25 | 26 | manifests 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /plugin/src/io/kipp/mill/github/dependency/graph/ModuleTrees.scala: -------------------------------------------------------------------------------- 1 | package io.kipp.mill.github.dependency.graph 2 | 3 | import com.github.packageurl.PackageURLBuilder 4 | import coursier.graph.DependencyTree 5 | import io.kipp.github.dependency.graph.domain._ 6 | import mill.scalalib.JavaModule 7 | 8 | import scala.collection.mutable 9 | import scala.util.Try 10 | 11 | /** Represents a project modules an the dependency trees that belong to it. 12 | * 13 | * @param module The module 14 | * @param dependencyTrees The dependency Trees belonging to the module 15 | * NOTE: that the roots of the trees are the direct dependencies. 16 | */ 17 | final case class ModuleTrees( 18 | module: JavaModule, 19 | dependencyTrees: Seq[DependencyTree] 20 | ) { 21 | 22 | /** Takes the dependencyTrees and flattens them to fit the model of the 23 | * DependencyNode that GitHub wants. They become flattened and every 24 | * dependency has a top level entry. Only the roots of the trees however 25 | * get a "direct" relationship. 26 | * 27 | * @return Mapping of the name of the dependency and the DependencyNode that 28 | * corresponds to it. The format of the name is org:module:version. 29 | */ 30 | def toFlattenedNodes()(implicit 31 | ctx: mill.api.Ctx 32 | ): Map[String, DependencyNode] = { 33 | 34 | // Keep track of every seen dependency and the DependencyNode for it 35 | val allDependencies = mutable.Map[String, DependencyNode]() 36 | // NOTE: maybe note necessary, but since we do this look in various times, we cache it 37 | val treeToName = mutable.Map[DependencyTree, String]() 38 | 39 | def toNode(tree: DependencyTree, root: Boolean): Unit = { 40 | 41 | def getNameFromTree(_tree: DependencyTree): String = { 42 | treeToName.getOrElseUpdate( 43 | _tree, { 44 | val _dep = _tree.dependency 45 | val moduleOrgName = _dep.module.orgName 46 | val reconciledVersion = _tree.reconciledVersion 47 | s"${moduleOrgName}:${reconciledVersion}" 48 | 49 | } 50 | ) 51 | } 52 | 53 | val dep = tree.dependency 54 | val name = getNameFromTree(tree) 55 | val reconciledVersion = tree.reconciledVersion 56 | val children = tree.children 57 | val childrenNames = children.map(getNameFromTree) 58 | 59 | def putTogether: DependencyNode = { 60 | // TODO consider classifiers 61 | 62 | val purl = Try( 63 | PackageURLBuilder 64 | .aPackageURL() 65 | .withType("maven") 66 | .withNamespace(dep.module.organization.value) 67 | .withName(dep.module.name.value) 68 | .withVersion(reconciledVersion) 69 | .build() 70 | ).fold( 71 | e => { 72 | ctx.log.error( 73 | s"PURL can't be created from: ${dep.module.orgName}:${reconciledVersion}" 74 | ) 75 | ctx.log.error(e.getMessage()) 76 | None 77 | }, 78 | validPurl => Some(validPurl.toString()) 79 | ) 80 | 81 | val relationShip: DependencyRelationship = 82 | if (root) DependencyRelationship.direct 83 | else DependencyRelationship.indirect 84 | 85 | DependencyNode( 86 | purl, 87 | // TODO we can check if original == reconciled here and add metadata that it is a reconciled version 88 | Map.empty, 89 | Some(relationShip), 90 | None, 91 | childrenNames 92 | ) 93 | } 94 | 95 | def verifyRelationship(node: DependencyNode) = 96 | (root && node.isDirectDependency) || (!root && !node.isDirectDependency) 97 | 98 | allDependencies.get(name) match { 99 | // If the node is found and the relationship is correct just do nothing 100 | case Some(node) if verifyRelationship(node) => 101 | ctx.log.debug( 102 | s"Already seen ${name} with this relationship in this manifest, so skipping..." 103 | ) 104 | // If the node is found and the relationship is incorrect, but it's a 105 | // root node, then make sure to mark it as direct 106 | case Some(node) if root => 107 | ctx.log.debug( 108 | s"Already seen ${name} but we're at the root level so marking as direct..." 109 | ) 110 | val updated = 111 | node.copy(relationship = Some(DependencyRelationship.direct)) 112 | allDependencies += ((name, updated)) 113 | case Some(_) => 114 | ctx.log.debug( 115 | s"Found ${name}, but it's already marked as direct so skipping..." 116 | ) 117 | // Not a very elegant check, but we don't want to include a range in 118 | // here. These shouldn't still be a range at this point, but it is for 119 | // whatever reason. For now ignore it. This should be incredibly rare 120 | // and I believe a bug in coursier. 121 | case None if reconciledVersion.contains(",") => 122 | ctx.log.error( 123 | s"""Found what I think is a range version that shouldn't be here... 124 | | 125 | |${dep.module.organization.value}:${dep.module.name.value}:${reconciledVersion} 126 | | 127 | |If you see this, report it. Skipping... 128 | |""".stripMargin 129 | ) 130 | // Unseen dependency, create a node for it 131 | case None => 132 | val node = putTogether 133 | allDependencies += ((name, node)) 134 | } 135 | 136 | // If all the children are already contained in allDependencies we don't even need 137 | // to try and process them, we just skip it and move on. 138 | if (childrenNames.forall(allDependencies.contains)) { 139 | ctx.log.debug( 140 | s"short circuiting as all children of ${name} are already looked at." 141 | ) 142 | } else { 143 | // This is a bit odd, but needed in the context of 144 | // https://github.com/ckipp01/mill-github-dependency-graph/issues/77 145 | // There can be poms that _look_ like they have cyclical dependencies 146 | // espeically when using classifiers. This actually seems like it might 147 | // be another bug in Couriser: 148 | // https://github.com/coursier/coursier/issues/2683 149 | // So, for now we filter out itself if it has itself listed as a child and we 150 | // also filter out any children that we've already seen. 151 | tree.children 152 | .filterNot(child => 153 | child == tree || 154 | allDependencies.contains(getNameFromTree(child)) 155 | ) 156 | .foreach(toNode(_, root = false)) 157 | } 158 | } 159 | 160 | dependencyTrees.foreach(toNode(_, root = true)) 161 | allDependencies.toMap 162 | } 163 | 164 | def toManifest()(implicit ctx: mill.api.Ctx): Manifest = { 165 | // NOTE: That this may seem odd when reading the spec that we have a 166 | // manifest per module basically, but we did check with the GitHub team and 167 | // they verified the manifests that we showed them. 168 | val name = module.toString() 169 | // TODO in the future we may want to also figure out how to resolve these 170 | // locations if they are defined in other files, but for now we just say build.sc 171 | val file = FileInfo("build.sc") 172 | val resolved = toFlattenedNodes() 173 | Manifest(name, Some(file), Map.empty, resolved) 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /plugin/src/io/kipp/mill/github/dependency/graph/Writers.scala: -------------------------------------------------------------------------------- 1 | package io.kipp.mill.github.dependency.graph 2 | 3 | import io.kipp.github.dependency.graph.domain.DependencyRelationship 4 | import io.kipp.github.dependency.graph.domain.DependencyScope 5 | import io.kipp.github.dependency.graph.domain.DependencySnapshot 6 | import io.kipp.github.dependency.graph.domain.Manifest 7 | import ujson.Obj 8 | import upickle.default.Writer 9 | 10 | /** Writers needed for Mill to show the result of the tasks, and also to 11 | * serialize the dependency snapshot. 12 | */ 13 | object Writers { 14 | 15 | /** So we do this by hand for a couple reason. We could rely on a macroW for 16 | * this, but we do then hit on some weird things and need a custom Option 17 | * handler and a couple other things. It ends up being just as short to 18 | * manually do all this, and then we avoid having to use ujson.Null as well. 19 | */ 20 | implicit val snapshotWriter: Writer[DependencySnapshot] = 21 | upickle.default.writer[ujson.Obj].comap { snapshot => 22 | ujson.Obj( 23 | "version" -> snapshot.version, 24 | "job" -> ujson.Obj( 25 | "id" -> snapshot.job.id, 26 | "correlator" -> snapshot.job.correlator, 27 | "html_url" -> snapshot.job.html_url.getOrElse[String]("") 28 | ), 29 | "sha" -> snapshot.sha, 30 | "ref" -> snapshot.ref, 31 | "detector" -> ujson.Obj( 32 | "name" -> snapshot.detector.name, 33 | "url" -> snapshot.detector.url, 34 | "version" -> snapshot.detector.version 35 | ), 36 | "metadata" -> snapshot.metadata.toJsonValue, 37 | "manifests" -> ujson.Obj.from(snapshot.manifests.map { 38 | case (key, manifest) => 39 | (key, manifestToJson(manifest)) 40 | }), 41 | "scanned" -> snapshot.scanned 42 | ) 43 | } 44 | 45 | implicit val manifestWriter: Writer[Manifest] = 46 | upickle.default.writer[ujson.Obj].comap(manifestToJson) 47 | 48 | private def manifestToJson(manifest: Manifest): Obj = { 49 | val base = ujson.Obj( 50 | "name" -> manifest.name, 51 | "metadata" -> manifest.metadata.toJsonValue, 52 | "resolved" -> ujson.Obj.from( 53 | manifest.resolved.map { case (key, dependencyNode) => 54 | val dependencyNodeObject = ujson.Obj( 55 | "metadata" -> dependencyNode.metadata.toJsonValue, 56 | "dependencies" -> ujson.Arr.from(dependencyNode.dependencies) 57 | ) 58 | dependencyNode.relationship.foreach { 59 | case DependencyRelationship.direct => 60 | dependencyNodeObject.update("relationship", "direct") 61 | case DependencyRelationship.indirect => 62 | dependencyNodeObject.update("relationship", "indirect") 63 | } 64 | dependencyNode.scope.foreach { 65 | case DependencyScope.development => 66 | dependencyNodeObject.update("scope", "development") 67 | case DependencyScope.runtime => 68 | dependencyNodeObject.update("scope", "runtime") 69 | } 70 | dependencyNode.package_url.foreach { url => 71 | dependencyNodeObject.update("package_url", url) 72 | } 73 | (key, dependencyNodeObject) 74 | } 75 | ) 76 | ) 77 | 78 | manifest.file.foreach { file => 79 | base.update( 80 | "file", 81 | ujson.Obj("source_location" -> file.source_location) 82 | ) 83 | } 84 | 85 | base 86 | } 87 | 88 | private implicit class MetadataJson(metadata: Map[String, String]) { 89 | def toJsonValue: Obj = { 90 | ujson.Obj.from( 91 | metadata.map { case (key, value) => 92 | (key, ujson.Str(value)) 93 | } 94 | ) 95 | } 96 | } 97 | 98 | } 99 | --------------------------------------------------------------------------------