Rewrite bot configuration
This commit is contained in:
parent
46a379d5a5
commit
7314cde1f4
10 changed files with 154 additions and 182 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -5,6 +5,7 @@
|
|||
build
|
||||
.idea/
|
||||
|
||||
config.json
|
||||
.env
|
||||
*.tar
|
||||
*.tar.gz
|
||||
|
|
|
@ -40,12 +40,12 @@ class App(
|
|||
}
|
||||
|
||||
private fun execute() {
|
||||
config.githubRepos.forEach { githubRepo ->
|
||||
log("Processing releases feed for ${githubRepo.name}...")
|
||||
val existingReleases = releaseRepo.getExistingReleases(githubRepo)
|
||||
val feedService = FeedService(githubRepo, httpClient)
|
||||
config.accounts.forEach { account ->
|
||||
log("Processing releases feed for ${account.repo.name}...")
|
||||
val existingReleases = releaseRepo.getExistingReleases(account.repo)
|
||||
val feedService = FeedService(account.repo, httpClient)
|
||||
val newReleases = feedService.getNewReleases(existingReleases)
|
||||
val publisher = publishers.forName(githubRepo.name)
|
||||
val publisher = publishers.forName(account.name)
|
||||
val publishedReleases = publisher.sendReleases(newReleases)
|
||||
releaseRepo.save(publishedReleases)
|
||||
log("Finished feed processing...")
|
||||
|
|
|
@ -1,82 +1,63 @@
|
|||
package de.rpr.githubreleases;
|
||||
package de.rpr.githubreleases
|
||||
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.GsonBuilder
|
||||
import de.rpr.githubreleases.LogLevel.ERROR
|
||||
import de.rpr.githubreleases.model.GithubRepo
|
||||
import de.rpr.terminenbg.LocalDateAdapter
|
||||
import de.rpr.terminenbg.LocalDateTimeAdapter
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalDateTime
|
||||
import java.io.InputStream
|
||||
import java.nio.file.Files
|
||||
|
||||
class Config {
|
||||
class Config(configInputStream: InputStream) {
|
||||
constructor(configFile: String = "config.json") : this(Files.newInputStream(configFile.toPath()))
|
||||
|
||||
data class Account(
|
||||
private val accountName: String?,
|
||||
private val github: String?,
|
||||
private val mastodonInstance: String?,
|
||||
private val mastodonAccessToken: String?,
|
||||
) {
|
||||
val repo get() = GithubRepo(github!!)
|
||||
val name get() = accountName!!
|
||||
val publishingInstance get() = mastodonInstance!!
|
||||
val publishingAccessToken get() = mastodonAccessToken!!
|
||||
|
||||
fun validate(): Boolean {
|
||||
var valid = true
|
||||
if (accountName.isNullOrEmpty()) {
|
||||
ERROR.log("Account should have a name defined.")
|
||||
valid = false
|
||||
}
|
||||
if (github.isNullOrEmpty()) {
|
||||
ERROR.log("Account should have a github repository defined.")
|
||||
valid = false
|
||||
}
|
||||
if (mastodonInstance.isNullOrEmpty()) {
|
||||
ERROR.log("Account should have a mastodon instance defined.")
|
||||
valid = false
|
||||
}
|
||||
if (mastodonAccessToken.isNullOrEmpty()) {
|
||||
ERROR.log("Account should have a mastodon access token defined.")
|
||||
valid = false
|
||||
}
|
||||
return valid
|
||||
}
|
||||
}
|
||||
|
||||
class ConfigFile(val accounts: List<Account>) {
|
||||
fun validate() = accounts.none { !it.validate() }
|
||||
}
|
||||
|
||||
@Transient
|
||||
private val gson: Gson = GsonBuilder().setPrettyPrinting().create()
|
||||
|
||||
data class PublishingCredentials(
|
||||
val instanceUrl: String?,
|
||||
@Transient val accessToken: String?,
|
||||
@Transient private val instanceUrlEnvName: String,
|
||||
@Transient private val accessTokenEnvName: String
|
||||
) {
|
||||
// Serialization helper
|
||||
@Suppress("unused")
|
||||
val token: String? = accessToken?.let { "${it.take(10)}..." }
|
||||
|
||||
@Transient
|
||||
var valid: Boolean = true
|
||||
|
||||
|
||||
init {
|
||||
validate()
|
||||
}
|
||||
|
||||
private fun validate() {
|
||||
if (instanceUrl.isNullOrBlank()) {
|
||||
ERROR.log("No instance url available, please set the environment variable $instanceUrlEnvName")
|
||||
valid = false
|
||||
}
|
||||
if (accessToken.isNullOrBlank()) {
|
||||
ERROR.log("No access token available, please set the environment variable $accessTokenEnvName")
|
||||
valid = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val githubRepos: List<GithubRepo>
|
||||
val mastodonCredentials: Map<String, PublishingCredentials>
|
||||
val accounts: List<Account>
|
||||
val schedulingDelay: Long = (System.getenv("SCHEDULING_DELAY")?.toLong() ?: 120) * 60
|
||||
|
||||
init {
|
||||
val configFile = gson.fromJson(configInputStream.reader(), ConfigFile::class.java)
|
||||
var valid = configFile.validate()
|
||||
|
||||
var valid = true
|
||||
|
||||
val githubReposString = System.getenv("GITHUB_REPOS")
|
||||
if (githubReposString == null) {
|
||||
ERROR.log("No github repos defined, please set repo urls in environment variable GITHUB_REPOS")
|
||||
throw IllegalStateException("Invalid configuration")
|
||||
|
||||
}
|
||||
|
||||
githubRepos = githubReposString.split(",").map {
|
||||
GithubRepo(it)
|
||||
}
|
||||
|
||||
mastodonCredentials = githubRepos.associate {
|
||||
val instanceUrlEnvName = "${it.name.uppercase()}_MASTODON_INSTANCE_URL"
|
||||
val accessTokenEnvName = "${it.name.uppercase()}_MASTODON_ACCESS_TOKEN"
|
||||
it.name to PublishingCredentials(
|
||||
instanceUrlEnvName = instanceUrlEnvName,
|
||||
instanceUrl = System.getenv(instanceUrlEnvName),
|
||||
accessTokenEnvName = accessTokenEnvName,
|
||||
accessToken = System.getenv(accessTokenEnvName)
|
||||
)
|
||||
}.onEach { (_, credentials) ->
|
||||
if (!credentials.valid) {
|
||||
valid = false
|
||||
}
|
||||
}
|
||||
accounts = configFile.accounts
|
||||
|
||||
if (schedulingDelay < 300) {
|
||||
ERROR.log("To avoid hammering the source webpage, scheduling delay has to be > 5 minutes")
|
||||
|
@ -91,6 +72,4 @@ class Config {
|
|||
override fun toString(): String {
|
||||
return "Config: " + gson.toJson(this)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,8 +2,8 @@ package de.rpr.githubreleases.publishing
|
|||
|
||||
import de.rpr.githubreleases.Config
|
||||
import de.rpr.githubreleases.LogLevel
|
||||
import de.rpr.githubreleases.model.Releases
|
||||
import de.rpr.githubreleases.log
|
||||
import de.rpr.githubreleases.model.Releases
|
||||
import social.bigbone.MastodonClient
|
||||
import social.bigbone.api.entity.data.Visibility
|
||||
import social.bigbone.api.exception.BigBoneRequestException
|
||||
|
@ -11,55 +11,46 @@ import social.bigbone.api.exception.BigBoneRequestException
|
|||
class Publisher(
|
||||
val name: String,
|
||||
private val client: MastodonClient,
|
||||
private val dryRun: Boolean = System.getenv("PUBLISH_DRY_RUN").toBoolean()
|
||||
private val dryRun: Boolean = System.getenv("PUBLISH_DRY_RUN").toBoolean(),
|
||||
) {
|
||||
|
||||
fun sendReleases(releases: Releases): Releases {
|
||||
log("${releases.size} new releases to publish")
|
||||
val result = releases
|
||||
.onEach { release -> log("Publishing release: ${release.title}") }
|
||||
.mapNotNull { release ->
|
||||
val request = client.statuses.postStatus(
|
||||
status = release.text,
|
||||
language = "en",
|
||||
visibility = Visibility.PUBLIC
|
||||
)
|
||||
try {
|
||||
if (!dryRun) {
|
||||
request.execute()
|
||||
} else {
|
||||
log("Dry-Run, skipping publishing of events...")
|
||||
val result =
|
||||
releases
|
||||
.onEach { release -> log("Publishing release: ${release.title}") }
|
||||
.mapNotNull { release ->
|
||||
val request =
|
||||
client.statuses.postStatus(
|
||||
status = release.text,
|
||||
language = "en",
|
||||
visibility = Visibility.PUBLIC,
|
||||
)
|
||||
try {
|
||||
if (!dryRun) {
|
||||
request.execute()
|
||||
} else {
|
||||
log("Dry-Run, skipping publishing of events...")
|
||||
}
|
||||
return@mapNotNull release
|
||||
} catch (ex: BigBoneRequestException) {
|
||||
log("ERROR: Event with id ${release.id} couldn't be published: " + ex.httpStatusCode)
|
||||
LogLevel.ERROR.log("Cause: ${ex.message}")
|
||||
LogLevel.ERROR.log("Root cause: ${ex.cause?.message}")
|
||||
return@mapNotNull null
|
||||
}
|
||||
return@mapNotNull release
|
||||
} catch (ex: BigBoneRequestException) {
|
||||
log("ERROR: Event with id ${release.id} couldn't be published: " + ex.httpStatusCode)
|
||||
LogLevel.ERROR.log("Cause: ${ex.message}")
|
||||
LogLevel.ERROR.log("Root cause: ${ex.cause?.message}")
|
||||
return@mapNotNull null
|
||||
}
|
||||
}
|
||||
return Releases(result)
|
||||
}
|
||||
}
|
||||
|
||||
class Publishers(
|
||||
private val config: Config,
|
||||
config: Config,
|
||||
private val mastodonClientFactory: MastodonClientFactory,
|
||||
) :
|
||||
Iterable<Publisher> {
|
||||
|
||||
private val instances: List<Publisher>
|
||||
|
||||
init {
|
||||
val mastodonClients = config.githubRepos.associate {
|
||||
val publishingCredentials = config.mastodonCredentials[it.name]!!
|
||||
it.name to mastodonClientFactory.getClient(
|
||||
publishingCredentials.instanceUrl!!,
|
||||
publishingCredentials.accessToken!!
|
||||
)
|
||||
) : Iterable<Publisher> {
|
||||
private val instances: List<Publisher> =
|
||||
config.accounts.map {
|
||||
Publisher(it.name, mastodonClientFactory.getClient(it.publishingInstance, it.publishingAccessToken))
|
||||
}
|
||||
instances = config.githubRepos.map { Publisher(it.name, mastodonClients[it.name]!!) }
|
||||
}
|
||||
|
||||
fun forName(name: String) = instances.first { it.name == name }
|
||||
|
||||
|
@ -67,4 +58,3 @@ class Publishers(
|
|||
return instances.iterator()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,49 +2,29 @@ package de.rpr.githubreleases
|
|||
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.containsExactly
|
||||
import assertk.assertions.containsOnly
|
||||
import de.rpr.githubreleases.model.GithubRepo
|
||||
import io.kotest.core.spec.style.DescribeSpec
|
||||
import io.kotest.extensions.system.withEnvironment
|
||||
import org.junit.jupiter.api.assertThrows
|
||||
|
||||
class ConfigTest : DescribeSpec({
|
||||
|
||||
describe("instantiation") {
|
||||
|
||||
withEnvironment(
|
||||
mapOf(
|
||||
"GITHUB_REPOS" to "https://github.com/navidrome/navidrome/",
|
||||
"NAVIDROME_MASTODON_INSTANCE_URL" to "example.com",
|
||||
"NAVIDROME_MASTODON_ACCESS_TOKEN" to "token"
|
||||
it("should populate accounts from config") {
|
||||
assertThat(testConfig.accounts).containsExactly(
|
||||
Config.Account(
|
||||
accountName = "navidrome",
|
||||
github = "navidrome/navidrome",
|
||||
mastodonInstance = "mastodon.social",
|
||||
mastodonAccessToken = "token",
|
||||
),
|
||||
)
|
||||
) {
|
||||
|
||||
it("should populate instances from config") {
|
||||
val config = Config()
|
||||
assertThat(config.githubRepos).containsExactly(
|
||||
GithubRepo("https://github.com/navidrome/navidrome/")
|
||||
)
|
||||
assertThat(config.mastodonCredentials).containsOnly(
|
||||
"navidrome" to Config.PublishingCredentials(
|
||||
instanceUrl = "example.com",
|
||||
accessToken = "token",
|
||||
instanceUrlEnvName = "NAVIDROME_MASTODON_INSTANCE_URL",
|
||||
accessTokenEnvName = "NAVIDROME_MASTODON_ACCESS_TOKEN"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
withEnvironment(
|
||||
mapOf("GITHUB_REPOS" to "https://github.com/navidrome/navidrome/")
|
||||
) {
|
||||
|
||||
it("should throw error if env variables for repo are not present") {
|
||||
assertThrows<IllegalStateException> {
|
||||
Config()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it("should throw error if account is invalid for repo are not present") {
|
||||
val configInputStream = Config::class.java.classLoader.getResourceAsStream("invalid-test-config.json")!!
|
||||
assertThrows<IllegalStateException> {
|
||||
Config(configInputStream)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
@ -6,9 +6,13 @@ import java.time.LocalDateTime
|
|||
|
||||
val testGithubRepo = GithubRepo("https://github.com/example/example")
|
||||
|
||||
val testRelease = Release(
|
||||
id = "id",
|
||||
link = "https://example.com",
|
||||
created = LocalDateTime.of(2023, 11, 29, 12, 11),
|
||||
githubRepo = testGithubRepo
|
||||
)
|
||||
val testRelease =
|
||||
Release(
|
||||
id = "id",
|
||||
link = "https://example.com",
|
||||
created = LocalDateTime.of(2023, 11, 29, 12, 11),
|
||||
githubRepo = testGithubRepo,
|
||||
)
|
||||
|
||||
private val configInputStream = Config::class.java.classLoader.getResourceAsStream("test-config.json")!!
|
||||
val testConfig = Config(configInputStream)
|
||||
|
|
|
@ -4,9 +4,9 @@ import assertk.assertThat
|
|||
import assertk.assertions.isEqualTo
|
||||
import assertk.assertions.isTrue
|
||||
import assertk.assertions.prop
|
||||
import de.rpr.githubreleases.Config
|
||||
import de.rpr.githubreleases.model.Releases
|
||||
import de.rpr.githubreleases.model.asCollection
|
||||
import de.rpr.githubreleases.testConfig
|
||||
import de.rpr.githubreleases.testRelease
|
||||
import io.kotest.core.spec.style.DescribeSpec
|
||||
import io.kotest.extensions.system.withEnvironment
|
||||
|
@ -37,12 +37,12 @@ class PublishersTest : DescribeSpec({
|
|||
mapOf(
|
||||
"GITHUB_REPOS" to "https://github.com/navidrome/navidrome/",
|
||||
"NAVIDROME_MASTODON_INSTANCE_URL" to "example.com",
|
||||
"NAVIDROME_MASTODON_ACCESS_TOKEN" to "token"
|
||||
)
|
||||
"NAVIDROME_MASTODON_ACCESS_TOKEN" to "token",
|
||||
),
|
||||
) {
|
||||
|
||||
it("should populate instances from config") {
|
||||
val publishers = Publishers(Config(), mastodonClientFactory)
|
||||
val publishers = Publishers(testConfig, mastodonClientFactory)
|
||||
assertThat(publishers.iterator().hasNext()).isTrue()
|
||||
assertThat(publishers.forName("navidrome"))
|
||||
.prop("name") { it.name }
|
||||
|
@ -62,7 +62,7 @@ class PublishersTest : DescribeSpec({
|
|||
spoilerText = any<String>(),
|
||||
status = any<String>(),
|
||||
language = any<String>(),
|
||||
visibility = any<Visibility>()
|
||||
visibility = any<Visibility>(),
|
||||
)
|
||||
} returns mockRequest
|
||||
|
||||
|
@ -77,7 +77,7 @@ class PublishersTest : DescribeSpec({
|
|||
sensitive = any(),
|
||||
spoilerText = any(),
|
||||
language = "en",
|
||||
addIdempotencyKey = any()
|
||||
addIdempotencyKey = any(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -88,7 +88,7 @@ class PublishersTest : DescribeSpec({
|
|||
spoilerText = any<String>(),
|
||||
status = any<String>(),
|
||||
language = any<String>(),
|
||||
visibility = any<Visibility>()
|
||||
visibility = any<Visibility>(),
|
||||
)
|
||||
} returns mockRequest
|
||||
|
||||
|
@ -105,11 +105,15 @@ class PublishersTest : DescribeSpec({
|
|||
sensitive = any(),
|
||||
spoilerText = any(),
|
||||
language = any(),
|
||||
addIdempotencyKey = any()
|
||||
addIdempotencyKey = any(),
|
||||
)
|
||||
}
|
||||
|
||||
assertThat(statusSlot.captured).isEqualTo("\uD83C\uDF89 Example v0.0.0 has been published!\n\nRelease notes are available here: https://example.com\n\n#Example")
|
||||
assertThat(statusSlot.captured)
|
||||
.isEqualTo(
|
||||
"\uD83C\uDF89 Example v0.0.0 has been published!" +
|
||||
"\n\nRelease notes are available here: https://example.com\n\n#Example",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -124,7 +128,7 @@ class PublishersTest : DescribeSpec({
|
|||
spoilerText = any<String>(),
|
||||
status = any<String>(),
|
||||
language = any<String>(),
|
||||
visibility = any<Visibility>()
|
||||
visibility = any<Visibility>(),
|
||||
)
|
||||
} returns mockRequest
|
||||
|
||||
|
@ -133,4 +137,4 @@ class PublishersTest : DescribeSpec({
|
|||
verify(exactly = 0) { mockRequest.execute() }
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
|
@ -20,7 +20,9 @@ import kotlin.io.path.listDirectoryEntries
|
|||
|
||||
class ReleaseRepositoryTest : DescribeSpec({
|
||||
|
||||
val sampleReleaseContent = """{
|
||||
val sampleReleaseContent =
|
||||
"""
|
||||
{
|
||||
"id": "v0.51.0",
|
||||
"link": "https://github.com/navidrome/navidrome/releases/tag/v0.51.0",
|
||||
"created": "2024-01-22T23:59:21Z",
|
||||
|
@ -29,7 +31,8 @@ class ReleaseRepositoryTest : DescribeSpec({
|
|||
"url": "https://github.com/navidrome/navidrome/"
|
||||
},
|
||||
"text": "🎉 Navidrome v0.51.0 has been published!\n\nRelease notes are available here: https://github.com/navidrome/navidrome/releases/tag/v0.51.0\n\n#Navidrome"
|
||||
}""".trimIndent()
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
val basePath = System.getProperty("java.io.tmpdir") + "/releases"
|
||||
|
||||
|
@ -38,18 +41,12 @@ class ReleaseRepositoryTest : DescribeSpec({
|
|||
}
|
||||
|
||||
afterSpec {
|
||||
basePath.toPath().listDirectoryEntries()
|
||||
.forEach {
|
||||
it.deleteExisting()
|
||||
}
|
||||
basePath.toPath().listDirectoryEntries().forEach { it.deleteExisting() }
|
||||
Files.deleteIfExists(basePath.toPath())
|
||||
}
|
||||
|
||||
afterTest {
|
||||
basePath.toPath().listDirectoryEntries()
|
||||
.forEach {
|
||||
it.deleteExisting()
|
||||
}
|
||||
basePath.toPath().listDirectoryEntries().forEach { it.deleteExisting() }
|
||||
}
|
||||
|
||||
describe("ReleaseRepository is created") {
|
||||
|
@ -70,9 +67,9 @@ class ReleaseRepositoryTest : DescribeSpec({
|
|||
id = "id",
|
||||
link = "https://example.com",
|
||||
created = LocalDateTime.now(),
|
||||
githubRepo = testGithubRepo
|
||||
)
|
||||
)
|
||||
githubRepo = testGithubRepo,
|
||||
),
|
||||
),
|
||||
)
|
||||
assertThat(basePath.toPath().listDirectoryEntries().map { it.fileName.toString() })
|
||||
.contains("${LocalDate.now()}-id.example.json")
|
||||
|
@ -86,9 +83,7 @@ class ReleaseRepositoryTest : DescribeSpec({
|
|||
|
||||
val postRepository = ReleaseRepository(basePath)
|
||||
val existingEvents = postRepository.getExistingReleases(testGithubRepo)
|
||||
assertThat(existingEvents).contains(testRelease.copy(id="v0.51.0"))
|
||||
assertThat(existingEvents).contains(testRelease.copy(id = "v0.51.0"))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
})
|
||||
})
|
||||
|
|
9
app/src/test/resources/invalid-test-config.json
Normal file
9
app/src/test/resources/invalid-test-config.json
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"accounts": [
|
||||
{
|
||||
"accountName": "navidrome",
|
||||
"github": "navidrome/navidrome",
|
||||
"mastodonAccessToken": "token"
|
||||
}
|
||||
]
|
||||
}
|
10
app/src/test/resources/test-config.json
Normal file
10
app/src/test/resources/test-config.json
Normal file
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"accounts": [
|
||||
{
|
||||
"accountName": "navidrome",
|
||||
"github": "navidrome/navidrome",
|
||||
"mastodonInstance": "mastodon.social",
|
||||
"mastodonAccessToken": "token"
|
||||
}
|
||||
]
|
||||
}
|
Loading…
Reference in a new issue