Skip to content

Commit 84d3660

Browse files
authored
Parallelize multi-arch Docker builds + HOCON (#32)
1 parent ea05ba6 commit 84d3660

File tree

9 files changed

+342
-32
lines changed

9 files changed

+342
-32
lines changed

.github/workflows/deploy.yml

Lines changed: 72 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,18 @@ on:
33
workflow_dispatch:
44

55
jobs:
6-
deploy_docker_jvm:
7-
runs-on: ubuntu-22.04
6+
build_jvm_matrix:
7+
strategy:
8+
matrix:
9+
include:
10+
- platform: linux/amd64
11+
runner: ubuntu-24.04
12+
- platform: linux/arm64
13+
runner: ubuntu-24.04-arm
14+
runs-on: ${{ matrix.runner }}
15+
permissions:
16+
contents: read
17+
packages: write
818
steps:
919
- uses: actions/checkout@v4
1020

@@ -18,14 +28,46 @@ jobs:
1828
username: ${{ github.actor }}
1929
password: ${{ secrets.GITHUB_TOKEN }}
2030

21-
- name: Build and Push JVM Docker images
31+
- name: Build and Push JVM Docker image for ${{ matrix.platform }}
2232
run: |
23-
make push-jvm
33+
make push-jvm-platform PLATFORM=${{ matrix.platform }}
2434
env:
2535
GIT_TAG: ${{ github.ref }}
2636

27-
deploy_docker_native:
37+
create_jvm_manifest:
38+
needs: build_jvm_matrix
2839
runs-on: ubuntu-22.04
40+
permissions:
41+
contents: read
42+
packages: write
43+
steps:
44+
- uses: actions/checkout@v4
45+
46+
- name: Login to GitHub Container Registry
47+
uses: docker/login-action@v3
48+
with:
49+
registry: ghcr.io
50+
username: ${{ github.actor }}
51+
password: ${{ secrets.GITHUB_TOKEN }}
52+
53+
- name: Create and Push JVM multi-platform manifest
54+
run: |
55+
make push-jvm-manifest
56+
env:
57+
GIT_TAG: ${{ github.ref }}
58+
59+
build_native_matrix:
60+
strategy:
61+
matrix:
62+
include:
63+
- platform: linux/amd64
64+
runner: ubuntu-24.04
65+
- platform: linux/arm64
66+
runner: ubuntu-24.04-arm
67+
runs-on: ${{ matrix.runner }}
68+
permissions:
69+
contents: read
70+
packages: write
2971
steps:
3072
- uses: actions/checkout@v4
3173

@@ -39,16 +81,38 @@ jobs:
3981
username: ${{ github.actor }}
4082
password: ${{ secrets.GITHUB_TOKEN }}
4183

42-
- name: Build and Push Native Docker images
84+
- name: Build and Push Native Docker image for ${{ matrix.platform }}
85+
run: |
86+
make push-native-platform PLATFORM=${{ matrix.platform }}
87+
env:
88+
GIT_TAG: ${{ github.ref }}
89+
90+
create_native_manifest:
91+
needs: build_native_matrix
92+
runs-on: ubuntu-22.04
93+
permissions:
94+
contents: read
95+
packages: write
96+
steps:
97+
- uses: actions/checkout@v4
98+
99+
- name: Login to GitHub Container Registry
100+
uses: docker/login-action@v3
101+
with:
102+
registry: ghcr.io
103+
username: ${{ github.actor }}
104+
password: ${{ secrets.GITHUB_TOKEN }}
105+
106+
- name: Create and Push Native multi-platform manifest
43107
run: |
44-
make push-native
108+
make push-native-manifest
45109
env:
46110
GIT_TAG: ${{ github.ref }}
47111

48112
all:
49113
name: Pushed All
50114
if: always()
51-
needs: [ deploy_docker_native, deploy_docker_jvm ]
115+
needs: [ create_jvm_manifest, create_native_manifest ]
52116
runs-on: ubuntu-22.04
53117
steps:
54118
- name: Validate required tests

.sdkmanrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
# Enable auto-env through the sdkman_auto_env config
22
# Add key=value pairs of SDKs to use below
3-
java=22-graalce
3+
java=21.0.9-tem

Makefile

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ IMG_NATIVE := ${NAME}:native-${TAG}
55
LATEST_JVM := ${NAME}:jvm-latest
66
LATEST_NATIVE := ${NAME}:native-latest
77
LATEST := ${NAME}:latest
8+
PLATFORM ?= linux/amd64,linux/arm64
89

910
dependency-updates:
1011
./gradlew dependencyUpdates \
@@ -23,6 +24,20 @@ build-jvm: init-docker
2324
push-jvm:
2425
DOCKER_EXTRA_ARGS="--push" $(MAKE) build-jvm
2526

27+
# Build and push for a single platform (used in matrix builds)
28+
build-jvm-platform: init-docker
29+
$(eval PLATFORM_TAG := $(shell echo ${PLATFORM} | tr '/' '-'))
30+
docker buildx build --platform ${PLATFORM} -f ./src/main/docker/Dockerfile.jvm -t "${IMG_JVM}-${PLATFORM_TAG}" -t "${LATEST_JVM}-${PLATFORM_TAG}" ${DOCKER_EXTRA_ARGS} .
31+
32+
push-jvm-platform:
33+
DOCKER_EXTRA_ARGS="--push" $(MAKE) build-jvm-platform
34+
35+
# Create and push multi-platform manifest combining platform-specific images
36+
push-jvm-manifest:
37+
docker buildx imagetools create -t "${IMG_JVM}" -t "${LATEST_JVM}" \
38+
"${IMG_JVM}-linux-amd64" \
39+
"${IMG_JVM}-linux-arm64"
40+
2641
build-jvm-local:
2742
docker build -f ./src/main/docker/Dockerfile.jvm -t "${IMG_JVM}" -t "${LATEST_JVM}" .
2843

@@ -35,6 +50,20 @@ build-native: init-docker
3550
push-native:
3651
DOCKER_EXTRA_ARGS="--push" $(MAKE) build-native
3752

53+
# Build and push for a single platform (used in matrix builds)
54+
build-native-platform: init-docker
55+
$(eval PLATFORM_TAG := $(shell echo ${PLATFORM} | tr '/' '-'))
56+
docker buildx build --platform ${PLATFORM} -f ./src/main/docker/Dockerfile.native -t "${IMG_NATIVE}-${PLATFORM_TAG}" -t "${LATEST_NATIVE}-${PLATFORM_TAG}" -t "${LATEST}-${PLATFORM_TAG}" ${DOCKER_EXTRA_ARGS} .
57+
58+
push-native-platform:
59+
DOCKER_EXTRA_ARGS="--push" $(MAKE) build-native-platform
60+
61+
# Create and push multi-platform manifest combining platform-specific images
62+
push-native-manifest:
63+
docker buildx imagetools create -t "${IMG_NATIVE}" -t "${LATEST_NATIVE}" -t "${LATEST}" \
64+
"${IMG_NATIVE}-linux-amd64" \
65+
"${IMG_NATIVE}-linux-arm64"
66+
3867
build-native-local:
3968
docker build -f ./src/main/docker/Dockerfile.native -t "${IMG_NATIVE}" -t "${LATEST_NATIVE}" -t "${LATEST}" .
4069

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ than 10 MB of RAM, so it can be installed on under-powered servers.
1616
> **NOTE**
1717
>
1818
> This used to be a Haskell project, that I switched to Kotlin. The code is still available on the [v1-haskell](https://github.com/alexandru/github-webhook-listener/tree/v1-haskell) branch.
19+
> There's also an experimental Rust branch, see [v3-rust](https://github.com/alexandru/github-webhook-listener/tree/v3-rust).
1920
2021
## Setup
2122

build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ dependencies {
7878
implementation(libs.kotlin.stdlib.jdk8)
7979
implementation(libs.kotlin.test.junit)
8080
implementation(libs.kotlinx.serialization.json)
81+
implementation(libs.kotlinx.serialization.hocon)
8182
implementation(libs.ktor.serialization.kotlinx.json)
8283
implementation(libs.ktor.server.cio)
8384
implementation(libs.ktor.server.core)

settings.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ dependencyResolutionManagement {
3838
.versionRef("kotlin")
3939
library("kotlinx-serialization-json", "org.jetbrains.kotlinx", "kotlinx-serialization-json")
4040
.versionRef("serialization")
41+
library("kotlinx-serialization-hocon", "org.jetbrains.kotlinx", "kotlinx-serialization-hocon")
42+
.versionRef("serialization")
4143

4244
// https://ktor.io/
4345
plugin("ktor", "io.ktor.plugin")

src/main/kotlin/org/alexn/hook/AppConfig.kt

Lines changed: 86 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
1+
@file:OptIn(ExperimentalSerializationApi::class)
2+
13
package org.alexn.hook
24

5+
import arrow.core.Either
36
import com.charleskorn.kaml.Yaml
47
import com.charleskorn.kaml.YamlConfiguration
8+
import com.typesafe.config.ConfigFactory
9+
import kotlinx.serialization.ExperimentalSerializationApi
510
import kotlinx.serialization.Serializable
11+
import kotlinx.serialization.hocon.Hocon
12+
import kotlinx.serialization.hocon.decodeFromConfig
613
import java.io.File
714
import kotlin.time.Duration
815

@@ -36,16 +43,77 @@ data class AppConfig(
3643
)
3744

3845
companion object {
39-
fun parseYaml(string: String): AppConfig =
40-
yamlParser.decodeFromString(
41-
serializer(),
42-
string,
43-
)
46+
fun parseFile(file: File) =
47+
when (file.extension.lowercase()) {
48+
"hocon", "conf" -> parseHocon(file)
49+
"yaml", "yml" -> parseYaml(file)
50+
else ->
51+
Either.Left(
52+
ConfigException(
53+
"Unsupported configuration file format: ${file.extension}",
54+
),
55+
)
56+
}
57+
58+
fun parseHocon(string: String): Either<ConfigException, AppConfig> =
59+
try {
60+
val r =
61+
Hocon.decodeFromConfig(
62+
serializer(),
63+
ConfigFactory.parseString(string).resolve(),
64+
)
65+
Either.Right(r)
66+
} catch (ex: Exception) {
67+
Either.Left(
68+
ConfigException(
69+
"Failed to parse HOCON configuration",
70+
ex,
71+
),
72+
)
73+
}
74+
75+
fun parseHocon(file: File): Either<ConfigException, AppConfig> =
76+
try {
77+
val txt = file.readText()
78+
parseHocon(txt)
79+
} catch (ex: Exception) {
80+
Either.Left(
81+
ConfigException(
82+
"Failed to read configuration file: ${file.absolutePath}",
83+
ex,
84+
),
85+
)
86+
}
87+
88+
fun parseYaml(string: String): Either<ConfigException, AppConfig> =
89+
try {
90+
Either.Right(
91+
yamlParser.decodeFromString(
92+
serializer(),
93+
string,
94+
),
95+
)
96+
} catch (ex: Exception) {
97+
Either.Left(
98+
ConfigException(
99+
"Failed to parse YAML configuration",
100+
ex,
101+
),
102+
)
103+
}
44104

45-
fun parseYaml(file: File): AppConfig {
46-
val txt = file.readText()
47-
return parseYaml(txt)
48-
}
105+
fun parseYaml(file: File): Either<ConfigException, AppConfig> =
106+
try {
107+
val txt = file.readText()
108+
parseYaml(txt)
109+
} catch (ex: Exception) {
110+
Either.Left(
111+
ConfigException(
112+
"Failed to read configuration file: ${file.absolutePath}",
113+
ex,
114+
),
115+
)
116+
}
49117

50118
private val yamlParser =
51119
Yaml(
@@ -56,3 +124,12 @@ data class AppConfig(
56124
)
57125
}
58126
}
127+
128+
/**
129+
* Exception thrown when there is a configuration error,
130+
* see [AppConfig].
131+
*/
132+
class ConfigException(
133+
message: String,
134+
cause: Throwable? = null,
135+
) : Exception(message, cause)

src/main/kotlin/org/alexn/hook/Main.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package org.alexn.hook
22

33
import arrow.continuations.SuspendApp
4+
import arrow.core.getOrElse
45
import com.github.ajalt.clikt.core.CliktCommand
56
import com.github.ajalt.clikt.core.Context
67
import com.github.ajalt.clikt.core.main
@@ -17,8 +18,8 @@ class RunServer :
1718

1819
override fun run() =
1920
SuspendApp {
20-
val config = AppConfig.parseYaml(File(configPath))
21-
startServer(config)
21+
val config = AppConfig.parseFile(File(configPath))
22+
startServer(config.getOrElse { throw it })
2223
}
2324
}
2425

0 commit comments

Comments
 (0)