3
3
@file:DependsOn(" io.github.typesafegithub:github-workflows-kt:3.3.0" )
4
4
@file:DependsOn(" it.krzeminski:snakeyaml-engine-kmp:3.1.1" )
5
5
@file:DependsOn(" io.github.optimumcode:json-schema-validator-jvm:0.5.1" )
6
+ @file:DependsOn(" com.github.sya-ri:kgit:1.1.0" )
6
7
7
8
@file:Repository(" https://bindings.krzeminski.it" )
8
9
@file:DependsOn(" actions:checkout:v4" )
9
10
@file:OptIn(ExperimentalKotlinLogicStep ::class )
10
11
@file:Suppress(" UNCHECKED_CAST" )
11
12
13
+ import com.github.syari.kgit.KGit
12
14
import io.github.optimumcode.json.schema.ErrorCollector
13
15
import io.github.optimumcode.json.schema.JsonSchema
14
16
import io.github.optimumcode.json.schema.ValidationError
@@ -19,19 +21,29 @@ import io.github.typesafegithub.workflows.domain.triggers.Cron
19
21
import io.github.typesafegithub.workflows.domain.triggers.PullRequest
20
22
import io.github.typesafegithub.workflows.domain.triggers.Push
21
23
import io.github.typesafegithub.workflows.domain.triggers.Schedule
24
+ import io.github.typesafegithub.workflows.dsl.expressions.expr
22
25
import io.github.typesafegithub.workflows.dsl.workflow
23
26
import it.krzeminski.snakeyaml.engine.kmp.api.Load
24
27
import kotlinx.serialization.json.JsonArray
25
28
import kotlinx.serialization.json.JsonElement
26
29
import kotlinx.serialization.json.JsonNull
27
30
import kotlinx.serialization.json.JsonObject
28
31
import 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
29
38
import java.io.File
30
39
import java.io.IOException
31
40
import java.net.URI
32
41
import java.nio.file.Files
42
+ import java.nio.file.Path
33
43
import java.util.Collections.emptySet
44
+ import java.util.stream.Stream
34
45
import kotlin.io.path.Path
46
+ import kotlin.io.path.extension
35
47
import kotlin.io.path.invariantSeparatorsPathString
36
48
import kotlin.io.path.name
37
49
@@ -67,8 +79,13 @@ workflow(
67
79
runsOn = UbuntuLatest ,
68
80
) {
69
81
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 })
72
89
}
73
90
}
74
91
@@ -106,7 +123,7 @@ private data class ActionCoords(
106
123
val pathToTypings : String ,
107
124
)
108
125
109
- private fun validateTypings () {
126
+ private fun validateTypings (sha : String , baseRef : String? ) {
110
127
val typingsSchema = JsonSchema .fromDefinition(
111
128
URI .create(" https://raw.githubusercontent.com/typesafegithub/github-actions-typing/" +
112
129
" refs/heads/schema-latest/github-actions-typing.schema.json"
@@ -117,27 +134,8 @@ private fun validateTypings() {
117
134
{ it.owner == " DamianReeves" && it.name == " write-file-action" },
118
135
)
119
136
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)
141
139
142
140
var shouldFail = false
143
141
@@ -199,6 +197,80 @@ private fun validateTypings() {
199
197
}
200
198
}
201
199
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
+
202
274
private fun loadTypings (path : String ): Map <String , Any > =
203
275
Load ().loadOne(File (path).readText()) as Map <String , Any >
204
276
0 commit comments