Skip to content

Commit 7d00a65

Browse files
NikkyAIkrzema12
andauthored
feat: add module with version update notifications (#1393)
A first version of the tool. --------- Co-authored-by: Piotr Krzemiński <[email protected]>
1 parent c677bdf commit 7d00a65

File tree

16 files changed

+609
-8
lines changed

16 files changed

+609
-8
lines changed

.github/workflows/build.main.kts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ workflow(
4646
run(
4747
name = "Build",
4848
command = "./gradlew build",
49+
env =
50+
linkedMapOf(
51+
"GITHUB_TOKEN" to expr("secrets.GITHUB_TOKEN"),
52+
),
4953
)
5054
}
5155
}

.github/workflows/build.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ jobs:
4141
cache-encryption-key: '${{ secrets.GRADLE_ENCRYPTION_KEY }}'
4242
- id: 'step-3'
4343
name: 'Build'
44+
env:
45+
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
4446
run: './gradlew build'
4547
build-for-Windows2022:
4648
runs-on: 'windows-2022'
@@ -61,6 +63,8 @@ jobs:
6163
cache-encryption-key: '${{ secrets.GRADLE_ENCRYPTION_KEY }}'
6264
- id: 'step-3'
6365
name: 'Build'
66+
env:
67+
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
6468
run: './gradlew build'
6569
publish-snapshot:
6670
name: 'Publish snapshot'

.github/workflows/end-to-end-tests.main.kts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#!/usr/bin/env kotlin
22
@file:Repository("file://~/.m2/repository/")
33
@file:DependsOn("io.github.typesafegithub:github-workflows-kt:1.14.1-SNAPSHOT")
4+
@file:DependsOn("io.github.typesafegithub:action-updates-checker:1.14.1-SNAPSHOT")
45
@file:Repository("https://github-workflows-kt-bindings.colman.com.br/binding/")
56
@file:DependsOn("actions:checkout:v4")
67
@file:DependsOn("actions:github-script:v7")
@@ -24,6 +25,7 @@ import io.github.typesafegithub.workflows.dsl.expressions.Contexts
2425
import io.github.typesafegithub.workflows.dsl.expressions.expr
2526
import io.github.typesafegithub.workflows.dsl.workflow
2627
import io.github.typesafegithub.workflows.yaml.writeToFile
28+
import io.github.typesafegithub.workflows.updates.reportAvailableUpdates
2729
import java.time.Instant
2830

2931
fun JobBuilder<*>.publishToMavenLocal() {
@@ -47,6 +49,9 @@ workflow(
4749
Push(branches = listOf("main")),
4850
PullRequest(),
4951
),
52+
yamlConsistencyJobEnv = linkedMapOf(
53+
"GITHUB_TOKEN" to expr("secrets.GITHUB_TOKEN")
54+
),
5055
yamlConsistencyJobAdditionalSteps = {
5156
publishToMavenLocal()
5257
},
@@ -246,4 +251,6 @@ workflow(
246251
""".trimIndent(),
247252
)
248253
}
254+
}.also { workflow ->
255+
workflow.reportAvailableUpdates()
249256
}.writeToFile()

.github/workflows/end-to-end-tests.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ jobs:
1212
check_yaml_consistency:
1313
name: 'Check YAML consistency'
1414
runs-on: 'ubuntu-latest'
15+
env:
16+
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
1517
steps:
1618
- id: 'step-0'
1719
name: 'Check out'
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
public final class io/github/typesafegithub/workflows/updates/ReportingKt {
2+
public static final fun reportAvailableUpdates (Lio/github/typesafegithub/workflows/domain/Workflow;ZLjava/lang/String;)V
3+
public static synthetic fun reportAvailableUpdates$default (Lio/github/typesafegithub/workflows/domain/Workflow;ZLjava/lang/String;ILjava/lang/Object;)V
4+
}
5+
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
plugins {
2+
buildsrc.convention.`kotlin-jvm`
3+
buildsrc.convention.publishing
4+
5+
id("org.jetbrains.kotlinx.binary-compatibility-validator") version "0.14.0"
6+
}
7+
8+
group = rootProject.group
9+
version = rootProject.version
10+
11+
dependencies {
12+
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0")
13+
14+
implementation(projects.githubWorkflowsKt)
15+
implementation(projects.sharedInternal)
16+
}
17+
18+
kotlin {
19+
explicitApi()
20+
}
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
package io.github.typesafegithub.workflows.updates
2+
3+
import java.io.File
4+
import kotlin.contracts.ExperimentalContracts
5+
import kotlin.contracts.InvocationKind
6+
import kotlin.contracts.contract
7+
8+
internal interface GithubStepSummary {
9+
fun appendText(text: String)
10+
11+
fun appendLine(line: String) {
12+
appendText(line + "\n")
13+
}
14+
15+
companion object {
16+
fun fromEnv(): GithubStepSummary? {
17+
val path = System.getenv("GITHUB_STEP_SUMMARY")
18+
return path
19+
?.let { File(it) }
20+
?.takeIf { it.exists() }
21+
?.let { githubStepSummaryFile ->
22+
object : GithubStepSummary {
23+
override fun appendText(text: String) {
24+
githubStepSummaryFile.appendText(text)
25+
}
26+
}
27+
}
28+
}
29+
}
30+
}
31+
32+
private val isGithubCi: Boolean by lazy {
33+
System.getenv("GITHUB_ACTIONS") == "true"
34+
}
35+
36+
internal fun logSimple(
37+
level: String,
38+
message: String,
39+
title: String? = null,
40+
file: String? = null,
41+
col: Int? = null,
42+
endColumn: Int? = null,
43+
line: Int? = null,
44+
endLine: Int? = null,
45+
) {
46+
val context =
47+
listOfNotNull(
48+
title?.let { "title=$it" },
49+
file?.let { "file=$it" },
50+
col?.let { "col=$it" },
51+
endColumn?.let { "endColumn=$it" },
52+
line?.let { "line=$it" },
53+
endLine?.let { "endLine=$it" },
54+
).joinToString(",")
55+
println("$level: $context $message")
56+
}
57+
58+
internal fun githubNotice(
59+
message: String,
60+
title: String? = null,
61+
file: String? = null,
62+
col: Int? = null,
63+
endColumn: Int? = null,
64+
line: Int? = null,
65+
endLine: Int? = null,
66+
) {
67+
if (isGithubCi) {
68+
val parameters =
69+
listOfNotNull(
70+
title?.let { "title=$it" },
71+
file?.let { "file=$it" },
72+
col?.let { "col=$it" },
73+
endColumn?.let { "endColumn=$it" },
74+
line?.let { "line=$it" },
75+
endLine?.let { "endLine=$it" },
76+
).joinToString(",")
77+
println("::notice $parameters::$message")
78+
} else {
79+
logSimple(
80+
level = "notice",
81+
message = message,
82+
title = title,
83+
file = file,
84+
col = col,
85+
endColumn = endColumn,
86+
line = line,
87+
endLine = endLine,
88+
)
89+
}
90+
}
91+
92+
internal fun githubWarning(
93+
message: String,
94+
title: String? = null,
95+
file: String? = null,
96+
col: Int? = null,
97+
endColumn: Int? = null,
98+
line: Int? = null,
99+
endLine: Int? = null,
100+
) {
101+
if (isGithubCi) {
102+
val parameters =
103+
listOfNotNull(
104+
title?.let { "title=$it" },
105+
file?.let { "file=$it" },
106+
col?.let { "col=$it" },
107+
endColumn?.let { "endColumn=$it" },
108+
line?.let { "line=$it" },
109+
endLine?.let { "endLine=$it" },
110+
).joinToString(",")
111+
println("::warning $parameters::$message")
112+
} else {
113+
logSimple(
114+
level = "warning",
115+
message = message,
116+
title = title,
117+
file = file,
118+
col = col,
119+
endColumn = endColumn,
120+
line = line,
121+
endLine = endLine,
122+
)
123+
}
124+
}
125+
126+
internal fun githubError(
127+
message: String,
128+
title: String? = null,
129+
file: String? = null,
130+
col: Int? = null,
131+
endColumn: Int? = null,
132+
line: Int? = null,
133+
endLine: Int? = null,
134+
) {
135+
if (isGithubCi) {
136+
val parameters =
137+
listOfNotNull(
138+
title?.let { "title=$it" },
139+
file?.let { "file=$it" },
140+
col?.let { "col=$it" },
141+
endColumn?.let { "endColumn=$it" },
142+
line?.let { "line=$it" },
143+
endLine?.let { "endLine=$it" },
144+
).joinToString(",")
145+
println("::error $parameters::$message")
146+
} else {
147+
logSimple(
148+
level = "error",
149+
message = message,
150+
title = title,
151+
file = file,
152+
col = col,
153+
endColumn = endColumn,
154+
line = line,
155+
endLine = endLine,
156+
)
157+
}
158+
}
159+
160+
@OptIn(ExperimentalContracts::class)
161+
internal fun githubGroup(
162+
title: String,
163+
block: () -> Unit,
164+
) {
165+
contract {
166+
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
167+
}
168+
if (isGithubCi) {
169+
println("::group::$title")
170+
} else {
171+
println(title)
172+
}
173+
block()
174+
if (isGithubCi) {
175+
println("::endgroup::")
176+
}
177+
}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
package io.github.typesafegithub.workflows.updates
2+
3+
import io.github.typesafegithub.workflows.domain.Workflow
4+
import io.github.typesafegithub.workflows.shared.internal.findGitRoot
5+
import io.github.typesafegithub.workflows.shared.internal.getGithubTokenOrNull
6+
import kotlinx.coroutines.flow.onEach
7+
import kotlinx.coroutines.flow.onEmpty
8+
import kotlinx.coroutines.flow.toList
9+
import kotlinx.coroutines.runBlocking
10+
import kotlin.io.path.absolute
11+
import kotlin.io.path.name
12+
import kotlin.io.path.pathString
13+
import kotlin.io.path.relativeTo
14+
15+
/**
16+
* will report all available updates in the terminal output and the github step summary
17+
* looks up the github token from env `GITHUB_TOKEN` by default
18+
* when no github token is present, reporting will be skipped
19+
*
20+
* @param reportWhenTokenUnset enable to use github api without a token
21+
* @param githubToken if not set, will try to load from the environment variable `GITHUB_TOKEN`
22+
*/
23+
public fun Workflow.reportAvailableUpdates(
24+
reportWhenTokenUnset: Boolean = false,
25+
githubToken: String? = null,
26+
): Unit =
27+
runBlocking {
28+
if (System.getenv("GHWKT_RUN_STEP") == null) {
29+
reportAvailableUpdatesInternal(
30+
reportWhenTokenUnset = reportWhenTokenUnset,
31+
githubToken = githubToken,
32+
)
33+
}
34+
}
35+
36+
internal suspend fun Workflow.reportAvailableUpdatesInternal(
37+
reportWhenTokenUnset: Boolean = false,
38+
githubToken: String? = null,
39+
stepSummary: GithubStepSummary? = GithubStepSummary.fromEnv(),
40+
) {
41+
availableVersionsForEachAction(
42+
reportWhenTokenUnset = reportWhenTokenUnset,
43+
githubToken = githubToken ?: getGithubTokenOrNull(),
44+
).onEach { regularActionVersions ->
45+
val usesString =
46+
with(regularActionVersions.action) {
47+
"$actionOwner/$actionName@$actionVersion"
48+
}
49+
50+
val stepNames = regularActionVersions.steps.map { it.name ?: it.id }
51+
52+
if (regularActionVersions.newerVersions.isEmpty()) {
53+
return@onEach
54+
}
55+
56+
githubGroup("new version available for $usesString") {
57+
val (file, line) = findDependencyDeclaration(regularActionVersions.action)
58+
59+
if (file != null && line != null) {
60+
githubNotice(
61+
message = "updates available for ${file.pathString}:$line",
62+
file = file.name,
63+
line = line,
64+
)
65+
}
66+
67+
if (stepSummary != null && file != null) {
68+
stepSummary.appendLine("## available updates for `$usesString`")
69+
stepSummary.appendLine("used by steps: ${stepNames.joinToString { "`$it`" }}")
70+
val githubRepo = System.getenv("GITHUB_REPOSITORY") ?: "\$GITHUB_REPOSITORY"
71+
val refName = System.getenv("GITHUB_REF_NAME") ?: "\$GITHUB_REF_NAME"
72+
val baseUrl = "https://github.com/$githubRepo/tree/$refName"
73+
val lineAnchor = line?.let { "#L$line" }.orEmpty()
74+
val gitRoot = file.findGitRoot()
75+
val relativeToRepositoryRoot =
76+
file.absolute().relativeTo(gitRoot.absolute())
77+
.joinToString("/")
78+
stepSummary.appendLine(
79+
"\n[${file.name}$lineAnchor]($baseUrl/${relativeToRepositoryRoot}$lineAnchor)",
80+
)
81+
}
82+
83+
stepSummary?.appendLine("\n```kotlin")
84+
regularActionVersions.newerVersions.forEach { version ->
85+
val mavenCoordinates = regularActionVersions.action.mavenCoordinatesForAction(version)
86+
println(mavenCoordinates)
87+
stepSummary?.appendLine("@file:DependsOn(\"$mavenCoordinates\")")
88+
}
89+
stepSummary?.appendLine("```\n")
90+
}
91+
}.onEmpty {
92+
githubNotice(
93+
message = "action-version-checker found no actions or skipped running",
94+
)
95+
}
96+
.toList()
97+
.also { regularActionVersions ->
98+
if (regularActionVersions.isNotEmpty()) {
99+
val hasOutdatedVersions =
100+
regularActionVersions
101+
.any { regularActionVersion ->
102+
regularActionVersion.newerVersions.isNotEmpty()
103+
}
104+
105+
if (!hasOutdatedVersions) {
106+
githubNotice(
107+
message = "action-version-checker found no outdated actions",
108+
)
109+
110+
stepSummary?.appendLine("action-version-checker found no outdated actions")
111+
}
112+
}
113+
}
114+
}

0 commit comments

Comments
 (0)