33@file:DependsOn(" io.github.typesafegithub:github-workflows-kt:3.3.0" )
44@file:DependsOn(" it.krzeminski:snakeyaml-engine-kmp:3.1.1" )
55@file:DependsOn(" io.github.optimumcode:json-schema-validator-jvm:0.5.1" )
6+ @file:DependsOn(" com.github.sya-ri:kgit:1.1.0" )
67
78@file:Repository(" https://bindings.krzeminski.it" )
89@file:DependsOn(" actions:checkout:v4" )
910@file:OptIn(ExperimentalKotlinLogicStep ::class )
1011@file:Suppress(" UNCHECKED_CAST" )
1112
13+ import com.github.syari.kgit.KGit
1214import io.github.optimumcode.json.schema.ErrorCollector
1315import io.github.optimumcode.json.schema.JsonSchema
1416import io.github.optimumcode.json.schema.ValidationError
@@ -19,19 +21,29 @@ import io.github.typesafegithub.workflows.domain.triggers.Cron
1921import io.github.typesafegithub.workflows.domain.triggers.PullRequest
2022import io.github.typesafegithub.workflows.domain.triggers.Push
2123import io.github.typesafegithub.workflows.domain.triggers.Schedule
24+ import io.github.typesafegithub.workflows.dsl.expressions.expr
2225import io.github.typesafegithub.workflows.dsl.workflow
2326import it.krzeminski.snakeyaml.engine.kmp.api.Load
2427import kotlinx.serialization.json.JsonArray
2528import kotlinx.serialization.json.JsonElement
2629import kotlinx.serialization.json.JsonNull
2730import kotlinx.serialization.json.JsonObject
2831import kotlinx.serialization.json.JsonPrimitive
32+ import org.eclipse.jgit.diff.DiffEntry.ChangeType.DELETE
33+ import org.eclipse.jgit.treewalk.CanonicalTreeParser
34+ import org.eclipse.jgit.treewalk.filter.AndTreeFilter
35+ import org.eclipse.jgit.treewalk.filter.OrTreeFilter
36+ import org.eclipse.jgit.treewalk.filter.PathFilter
37+ import org.eclipse.jgit.treewalk.filter.PathSuffixFilter
2938import java.io.File
3039import java.io.IOException
3140import java.net.URI
3241import java.nio.file.Files
42+ import java.nio.file.Path
3343import java.util.Collections.emptySet
44+ import java.util.stream.Stream
3445import kotlin.io.path.Path
46+ import kotlin.io.path.extension
3547import kotlin.io.path.invariantSeparatorsPathString
3648import kotlin.io.path.name
3749
@@ -67,8 +79,13 @@ workflow(
6779 runsOn = UbuntuLatest ,
6880 ) {
6981 uses(action = Checkout ())
70- run (name = " Check for all actions" ) {
71- validateTypings()
82+ run (
83+ name = " Check for actions" ,
84+ // TODO: replace this workaround once base_ref can be accessed natively:
85+ // https://github.com/typesafegithub/github-workflows-kt/issues/1946
86+ env = mapOf (" base_ref" to expr { github.base_ref })
87+ ) {
88+ validateTypings(github.sha, System .getenv(" base_ref" ).ifEmpty { null })
7289 }
7390 }
7491
@@ -106,7 +123,7 @@ private data class ActionCoords(
106123 val pathToTypings : String ,
107124)
108125
109- private fun validateTypings () {
126+ private fun validateTypings (sha : String , baseRef : String? ) {
110127 val typingsSchema = JsonSchema .fromDefinition(
111128 URI .create(" https://raw.githubusercontent.com/typesafegithub/github-actions-typing/" +
112129 " refs/heads/schema-latest/github-actions-typing.schema.json"
@@ -117,27 +134,8 @@ private fun validateTypings() {
117134 { it.owner == " DamianReeves" && it.name == " write-file-action" },
118135 )
119136
120-
121- val actionsWithYamlExtension = Files .walk(Path (" typings" ))
122- .filter { it.name == " action-types.yaml" }
123- .toList()
124- check(actionsWithYamlExtension.isEmpty()) {
125- " Some files have .yaml extension, and we'd like to use only .yml here: $actionsWithYamlExtension "
126- }
127-
128- val actions = Files .walk(Path (" typings" ))
129- .filter { it.name == " action-types.yml" }
130- .map {
131- val (_, owner, name, version, pathAndYaml) = it.invariantSeparatorsPathString.split(" /" , limit = 5 )
132- val path = if (" /" in pathAndYaml) pathAndYaml.substringBeforeLast(" /" ) else null
133- ActionCoords (
134- owner = owner,
135- name = name,
136- version = version,
137- path = path,
138- pathToTypings = it.invariantSeparatorsPathString,
139- )
140- }
137+ println ()
138+ val actions = listActionsToValidate(sha = sha, baseRef = baseRef)
141139
142140 var shouldFail = false
143141
@@ -199,6 +197,80 @@ private fun validateTypings() {
199197 }
200198}
201199
200+ private fun listActionsToValidate (sha : String , baseRef : String? ): Stream <ActionCoords > =
201+ baseRef.let { baseRef ->
202+ if (baseRef == null ) {
203+ println (" Validating all typings" )
204+ listAllActionManifestFilesInRepo()
205+ } else {
206+ println (" Only validating changed typings" )
207+ listAffectedActionManifestFiles(sha = sha, baseRef = baseRef)
208+ }.map {
209+ val (_, owner, name, version, pathAndYaml) = it.invariantSeparatorsPathString.split(" /" , limit = 5 )
210+ val path = if (" /" in pathAndYaml) pathAndYaml.substringBeforeLast(" /" ) else null
211+ ActionCoords (
212+ owner = owner,
213+ name = name,
214+ version = version,
215+ path = path,
216+ pathToTypings = it.invariantSeparatorsPathString,
217+ )
218+ }
219+ }
220+
221+ private fun listAllActionManifestFilesInRepo (): Stream <Path > {
222+ val actionsWithYamlExtension = Files .walk(Path (" typings" ))
223+ .filter { it.name == " action-types.yaml" }
224+ .toList()
225+ check(actionsWithYamlExtension.isEmpty()) {
226+ " Some files have .yaml extension, and we'd like to use only .yml here: $actionsWithYamlExtension "
227+ }
228+
229+ return Files .walk(Path (" typings" )).filter { it.name == " action-types.yml" }
230+ }
231+
232+ private fun listAffectedActionManifestFiles (sha : String , baseRef : String? ): Stream <Path > {
233+ val typings = try {
234+ KGit .open(File (" ." )).use { git ->
235+ git.fetch {
236+ setRefSpecs(" refs/heads/$baseRef :refs/heads/$baseRef " )
237+ setDepth(1 )
238+ }
239+ git.diff {
240+ setShowNameAndStatusOnly(true )
241+ git.repository.newObjectReader().use { objectReader ->
242+ setOldTree(CanonicalTreeParser ().apply {
243+ reset(objectReader, git.repository.resolve(" refs/heads/$baseRef ^{tree}" ))
244+ })
245+ setNewTree(CanonicalTreeParser ().apply {
246+ reset(objectReader, git.repository.resolve(" $sha ^{tree}" ))
247+ })
248+ }
249+ setPathFilter(
250+ AndTreeFilter .create(
251+ PathFilter .create(" typings/" ),
252+ OrTreeFilter .create(
253+ PathSuffixFilter .create(" /action-types.yml" ),
254+ PathSuffixFilter .create(" /action-types.yaml" ),
255+ ),
256+ )
257+ )
258+ }
259+ .filter { it.changeType != DELETE }
260+ .map { Path (it.newPath) }
261+ .groupBy { it.extension == " yml" }
262+ }
263+ } finally {
264+ KGit .shutdown()
265+ }
266+
267+ check(typings[false ].isNullOrEmpty()) {
268+ " Some files have .yaml extension, and we'd like to use only .yml here: ${typings[false ]} "
269+ }
270+
271+ return typings[true ]?.stream() ? : Stream .of()
272+ }
273+
202274private fun loadTypings (path : String ): Map <String , Any > =
203275 Load ().loadOne(File (path).readText()) as Map <String , Any >
204276
0 commit comments