1
+ #!/usr/bin/env -S scala-cli shebang
2
+ //> using scala 3.3
3
+ //> using toolkit 0.1.7
4
+ //> using dep io.get-coursier:coursier_2.13:2.1.4
5
+ //> using file Dependencies.scala
6
+ //> using file Config.scala
7
+
8
+ import scala .util .Try
9
+ import coursier .*
10
+ import coursier .given
11
+ import coursier .graph .DependencyTree
12
+ import scala .util .matching .Regex
13
+ import upickle .default .*
14
+ import scala .io .StdIn .readLine
15
+ import Dependencies .*
16
+
17
+ Utility .requireCmd(" scala-cli" )
18
+
19
+ case class DiffSummary (updateType : VersionDiff , diffs : List [Diff ], illegalDiffs : List [IllegalDiff ])
20
+ sealed trait Diff derives ReadWriter :
21
+ def under : Option [Dep ]
22
+ case class Added (newDep : Dep , under : Option [Dep ]) extends Diff :
23
+ override def toString : String = s " Added ` $newDep` ${under.fold(" " )(u => s " under ` $u` " )}"
24
+ case class Removed (oldDep : Dep , under : Option [Dep ]) extends Diff :
25
+ override def toString : String = s " Removed ` $oldDep` ${under.fold(" " )(u => s " under ` $u` " )}"
26
+ case class DepUpdated (oldDep : Dep , newDep : Dep , under : Option [Dep ]) extends Diff :
27
+ override def toString : String =
28
+ s " Updated ` $oldDep` from ` ${oldDep.version}` to ` ${newDep.version}` ${under.fold(" " )(u => s " under ` $u` " )}"
29
+
30
+ object Diff :
31
+ def getOldDep (diff : Diff ) = diff match
32
+ case Added (newDep, _) => None
33
+ case Removed (oldDep, _) => Some (oldDep)
34
+ case DepUpdated (oldDep, _, _) => Some (oldDep)
35
+
36
+ case class IllegalDiff (diff : Diff , leastOrderLegalUpdate : VersionDiff ):
37
+ override def toString : String = s " $diff (required at least: $leastOrderLegalUpdate) "
38
+
39
+ case class Params (file : String , overwrite : Boolean )
40
+
41
+ val params = args match
42
+ case Array (file) => Params (file, false )
43
+ case Array (file, " --overwrite" ) => Params (file, true )
44
+ case _ => throw new Exception (" Usage: ./scripts/changelog.sc <file> [overwrite]" )
45
+
46
+ println(" Publishing locally to validate the dependency tree..." )
47
+ os.proc(" scala-cli" , " --power" , " publish" , " local" , " --cross" , " --organization" , Config .organization, " --version" , Config .developmentVersion.toString, params.file).call()
48
+
49
+ Config .crossVersions.foreach(checkTree)
50
+
51
+ def checkTree (crossVersion : String ) =
52
+
53
+ val previousSnapshot = Dep .resolve(Config .organization, Config .name, crossVersion, Config .releaseVersion)
54
+ val currentSnapshot = Dep .resolve(Config .organization, Config .name, crossVersion, Config .developmentVersion)
55
+
56
+ val summary = SnapshotDiffValidator .summarize(previousSnapshot, currentSnapshot)
57
+ if summary.illegalDiffs.nonEmpty then
58
+ println(" Found diffs illegal to introduce on this update type: " + summary.updateType)
59
+ println(s " Illegal diffs: \n ${summary.illegalDiffs.mkString(" - " , " \n - " , " \n " )}" )
60
+ if params.overwrite then
61
+ println(" Could not generate changelog due to illegal diffs." )
62
+ println(" Exiting with failure status (1)" )
63
+ sys.exit(1 )
64
+
65
+ val diffsFound = summary.diffs.nonEmpty
66
+
67
+ if diffsFound then
68
+ println(" Found diffs in development version: \n " + summary.diffs.mkString(" - " , " \n - " , " \n " ))
69
+ else
70
+ println(" No diffs found" )
71
+
72
+ val generatedChangelog = Changelog .generate(summary.diffs)
73
+ val changelogJson = os.pwd / " changelog" / " json" / s " ${Config .developmentVersion}_changelog_ ${crossVersion}.json "
74
+ if ! os.exists(changelogJson) then
75
+ println(" No changelog found, comparing with empty changelog." )
76
+ os.makeDir.all(os.pwd / " changelog" / " json" )
77
+ val parsedChangelog = if (os.exists(changelogJson)) then read[Changelog ](os.read(changelogJson)) else Changelog (Set .empty, Set .empty)
78
+ def changelogsDiff = generatedChangelog.diff(parsedChangelog)
79
+
80
+ if changelogsDiff.nonEmpty then
81
+ println(" Found diffs in changelog: \n " + changelogsDiff.get)
82
+ if ! params.overwrite then
83
+ println(" Exiting with failure status (1). Please run with --overwrite to overwrite the expected changelog." )
84
+ sys.exit(1 )
85
+ else
86
+ val input = readLine(" Overwrite symbols? (y/N)" )
87
+ println(s " Input: $input" )
88
+ if input == " y" || input == " Y" then
89
+ os.write.over(changelogJson, write(generatedChangelog))
90
+ val changelogReadable = generatedChangelog.toMd
91
+ val changelogMd = os.pwd / " changelog" / s " ${Config .developmentVersion}_changelog_ ${crossVersion}.md "
92
+ os.write.over(changelogMd, changelogReadable)
93
+ println(" Overwritten" )
94
+ else
95
+ println(" Changes rejected" )
96
+ sys.exit(1 )
97
+ else
98
+ println(" No diffs found in changelog" )
99
+
100
+
101
+ object SnapshotDiffValidator :
102
+
103
+ def summarize (previous : Dep , current : Dep ): DiffSummary =
104
+ val versionDiff = Version .compareVersions(previous.version, current.version)
105
+
106
+ def traverseDep (oldDep : Option [Dep ], newDep : Option [Dep ], enteredFrom : Option [Dep ] = None ): List [Diff ] =
107
+ def traverseDepTupled (enteredFrom : Dep )(pair : (Option [Dep ], Option [Dep ])) = traverseDep(pair._1, pair._2, Some (enteredFrom))
108
+ (oldDep, newDep) match
109
+ case (Some (oldDep), Some (newDep)) =>
110
+ if oldDep.version == newDep.version then Nil
111
+ else
112
+ val diff = DepUpdated (oldDep, newDep, enteredFrom)
113
+ val pairs = oldDep.deps.map(dep => (Some (dep), newDep.deps.find(_.id == dep.id)))
114
+ val newDeps = newDep.deps.filterNot(dep => oldDep.deps.exists(_.id == dep.id)).map(dep => (None , Some (dep)))
115
+ val diffs = pairs.flatMap(traverseDepTupled(newDep)) ++ newDeps.flatMap(traverseDepTupled(newDep))
116
+ diff :: diffs
117
+ case (Some (oldDep), None ) => Removed (oldDep, enteredFrom) :: oldDep.deps.flatMap(dep => traverseDep(Some (dep), None , Some (oldDep))).toList
118
+ case (None , Some (newDep)) => Added (newDep, enteredFrom) :: newDep.deps.flatMap(dep => traverseDep(None , Some (dep), Some (newDep))).toList
119
+ case _ => Nil
120
+
121
+ val diffs : List [Diff ] = traverseDep(Some (previous), Some (current))
122
+ val illegalDiffs : List [IllegalDiff ] = diffs
123
+ .map(diff => IllegalDiff (diff, getLeastOrderLegalUpdate(versionDiff, diff)))
124
+ .filter(_.leastOrderLegalUpdate > versionDiff)
125
+ DiffSummary (versionDiff, diffs, illegalDiffs)
126
+
127
+
128
+ def getLeastOrderLegalUpdate (versionDiff : VersionDiff , diff : Diff ): VersionDiff =
129
+ diff match
130
+ case _ : Removed => MajorUpdate
131
+ case _ : Added => MinorUpdate
132
+ case DepUpdated (oldDep, newDep, _) =>
133
+ Version .compareVersions(oldDep.version, newDep.version)
134
+
135
+
136
+ case class Changelog (directChanges : Set [Diff ], indirectChanges : Set [Diff ]) derives ReadWriter :
137
+
138
+ def diff (other : Changelog ): Option [String ] =
139
+ if other == this then
140
+ None
141
+ else
142
+ val directAdded = directChanges.filterNot(other.directChanges.contains)
143
+ val directRemoved = other.directChanges.filterNot(directChanges.contains)
144
+ val indirectAdded = indirectChanges.filterNot(other.indirectChanges.contains)
145
+ val indirectRemoved = other.indirectChanges.filterNot(indirectChanges.contains)
146
+ val directAddedString = if directAdded.isEmpty then " " else s " - Direct changes added: \n ${directAdded.mkString(" - " , " \n - " , " " )}\n "
147
+ val directRemovedString = if directRemoved.isEmpty then " " else s " - Direct changes removed: \n ${directRemoved.mkString(" - " , " \n - " , " " )}\n "
148
+ val indirectAddedString = if indirectAdded.isEmpty then " " else s " - Indirect changes added: \n ${indirectAdded.mkString(" - " , " \n - " , " " )}\n "
149
+ val indirectRemovedString = if indirectRemoved.isEmpty then " " else s " - Indirect changes removed: \n ${indirectRemoved.mkString(" - " , " \n - " , " " )}\n "
150
+ Some (directAddedString + directRemovedString + indirectAddedString + indirectRemovedString)
151
+
152
+ def toMd : String =
153
+ val directChangesSection = if directChanges.isEmpty then " " else
154
+ val directChangesHeader = " ## Changes to direct dependencies"
155
+ val directChangesList = directChanges.map(diff => s " - ${diff.toString}" ).toList.sorted
156
+ (directChangesHeader :: directChangesList).mkString(" \n " )
157
+ val indirectChangesSection = if indirectChanges.isEmpty then " " else
158
+ val indirectChangesHeader = " ## Changes to transitive dependencies"
159
+ val indirectChangesList = indirectChanges.map(diff => s " - ${diff.toString}" ).toList.sorted
160
+ (indirectChangesHeader :: indirectChangesList).mkString(" \n " )
161
+ val header = s " # Changelog for ${Config .name} ${Config .developmentVersion}"
162
+ header + " \n " + directChangesSection + " \n " + indirectChangesSection
163
+
164
+ object Changelog :
165
+ def generate (diffs : List [Diff ]): Changelog =
166
+ val parentDiff = diffs.find(_.under.isEmpty).getOrElse(throw new Exception (" No parent diff found" ))
167
+ val parentId = Diff .getOldDep(parentDiff).getOrElse(throw new Exception (s " Illegal parent diff: ${parentDiff}" )).id
168
+ val (directChanges, indirectChanges) = diffs.partition(_.under.exists(_.id == parentId))
169
+ Changelog (directChanges.toSet, indirectChanges.toSet - parentDiff)
0 commit comments