ci: UTF-8 локаль + ASCII-дефис в именах тестов; release через общий publish action
ci / JVM build + tests (push) Failing after 2m44s

- ci.yml/release.yml: LANG/LC_ALL=C.UTF-8 — иначе Kotlin-компилятор падает
  с InvalidPathException на именах тестов с типографским тире (LANG=C → ASCII)
- имена тестов: типографское тире U+2014 заменено на ASCII-дефис (16 шт)
- release.yml переведён на общий composite-action subochev/devops/publish@main
  (как у asr-kmp/litert-kmp/embedder-kmp); версия = имя тега релиза
This commit is contained in:
2026-09-18 22:15:51 +03:00
parent 2634e0e204
commit 741ad8963d
6 changed files with 45 additions and 64 deletions
+8
View File
@@ -17,6 +17,14 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true cancel-in-progress: true
# UTF-8 обязателен: в именах тестов есть типографские символы (—), а Kotlin-компилятор
# создаёт .class-файлы с именем теста. При LANG=C sun.jnu.encoding = ASCII, и компилятор
# падает с "InvalidPathException: Malformed input or input contains unmappable characters"
# (проверено локально: LANG=C → BUILD FAILED, LANG=C.UTF-8 → BUILD SUCCESSFUL).
env:
LANG: C.UTF-8
LC_ALL: C.UTF-8
jobs: jobs:
build-jvm: build-jvm:
name: JVM build + tests name: JVM build + tests
+19 -46
View File
@@ -8,10 +8,13 @@
# :<module>:shadowJar`) и загружается в релиз через Gitea UI / API # :<module>:shadowJar`) и загружается в релиз через Gitea UI / API
# отдельно от этого workflow. # отдельно от этого workflow.
# #
# Требуемые Gitea Action Variables: # Версия публикации = имя тега релиза (без префикса 'v'). Релиз с именем "3"
# BINOM_REPO_URL — например http://192.168.76.117/repository/caffeine/ # публикует pw.binom.agentik:*:3 в Nexus. Ничего хардкодить не нужно —
# Требуемые Gitea Action Secrets: # версия берётся из тега каждый раз.
# BINOM_REPO_USER, BINOM_REPO_PASSWORD — креды Nexus с правами на публикацию. #
# Публикация выполняется общим composite-action'ом subochev/devops/publish@main
# (тот же, что у asr-kmp / litert-kmp / embedder-kmp / a2a-protocol) — credentials
# BINOM_REPO_* берутся им из Gitea Action Variables (owner_id=0, глобальные).
name: release name: release
on: on:
@@ -22,6 +25,15 @@ concurrency:
group: release-${{ github.ref }} group: release-${{ github.ref }}
cancel-in-progress: false cancel-in-progress: false
# UTF-8 обязателен: генерация POM/Kotlin-метаданных и имена тестовых классов
# содержат не-ASCII символы; при LANG=C sun.jnu.encoding = ASCII и сборка
# падает с "InvalidPathException: Malformed input or input contains unmappable
# characters" (проверено локально 19.09.2026: LANG=C → BUILD FAILED,
# LANG=C.UTF-8 → BUILD SUCCESSFUL).
env:
LANG: C.UTF-8
LC_ALL: C.UTF-8
jobs: jobs:
publish-libraries: publish-libraries:
name: Publish KMP libraries → caffeine Nexus name: Publish KMP libraries → caffeine Nexus
@@ -31,46 +43,7 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Setup JDK 21 - name: Publish libraries (all KMP targets, all modules) to Nexus
uses: actions/setup-java@v4 uses: https://git.binom.pw/subochev/devops/publish@main
with: with:
java-version: '21' version: ${{ gitea.ref_name }}
distribution: 'adopt'
- name: Gradle cache
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
.gradle
key: ${{ runner.os }}-gradle-agentik-${{ hashFiles('**/*.gradle.kts', '**/gradle/libs.versions.toml', 'gradle/wrapper/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-agentik-
- name: Publish libraries (all KMP targets, all modules)
shell: bash
env:
BINOM_REPO_USER: ${{ secrets.BINOM_REPO_USER }}
BINOM_REPO_PASSWORD: ${{ secrets.BINOM_REPO_PASSWORD }}
BINOM_REPO_URL: ${{ vars.BINOM_REPO_URL }}
run: |
# Gitea Actions (Forgejo-based) экспонирует env-переменные под
# GITHUB_-префиксом: GITHUB_REF_NAME = "v0.1.0" для tag-trigger'а.
# Внутри bash подставляем через $GITHUB_REF_NAME (а не
# ${GITEA_REF_NAME} — Forgejo этого не подставляет).
#
# Версия = имя тега (с trim'ом опционального префикса 'v'), чтобы
# тег "1" публиковался как pw.binom.agentik:<module>:1. CICD не
# хардкодит версию — берёт её из тега каждый раз.
TAG="$GITHUB_REF_NAME"
VERSION="${TAG#v}"
echo "Publishing version: ${VERSION}"
./gradlew \
"-Pversion=${VERSION}" \
"-Pbinom.repo.url=${BINOM_REPO_URL}" \
"-Pbinom.repo.user=${BINOM_REPO_USER}" \
"-Pbinom.repo.password=${BINOM_REPO_PASSWORD}" \
publish \
-Dorg.gradle.jvmargs=-Xmx4096M \
--no-daemon --no-watch-fs --stacktrace
@@ -205,7 +205,7 @@ class TuiBackendTest {
} }
@Test @Test
fun `onUserMessage does not swallow exceptions — state stays consistent`() = runTest { fun `onUserMessage does not swallow exceptions - state stays consistent`() = runTest {
val cfg = fixtureConfig() val cfg = fixtureConfig()
val state = AppState(cfg) val state = AppState(cfg)
val agent = FakeAgent(conversationFactory = { error("server kaboom") }) val agent = FakeAgent(conversationFactory = { error("server kaboom") })
@@ -555,7 +555,7 @@ class ChatAgentTest {
} }
@Test @Test
fun `agentEvents — Created + Deleted flow`() = runTest { fun `agentEvents - Created + Deleted flow`() = runTest {
val agent = newAgent() val agent = newAgent()
val events = mutableListOf<AgentEvent>() val events = mutableListOf<AgentEvent>()
val job = launch(start = kotlinx.coroutines.CoroutineStart.UNDISPATCHED) { val job = launch(start = kotlinx.coroutines.CoroutineStart.UNDISPATCHED) {
@@ -17,7 +17,7 @@ class LlmConfigTest {
} }
@Test @Test
fun `fromEnv — happy path`() { fun `fromEnv - happy path`() {
val cfg = AppConfig.fromEnv { name -> val cfg = AppConfig.fromEnv { name ->
when (name) { when (name) {
"OPENAI_BASE_URL" -> "https://api.openai.com/v1" "OPENAI_BASE_URL" -> "https://api.openai.com/v1"
@@ -32,13 +32,13 @@ class LlmConfigTest {
} }
@Test @Test
fun `fromEnv — falls back to default system prompt`() { fun `fromEnv - falls back to default system prompt`() {
val cfg = AppConfig.fromEnv(openAiEnv()).llm val cfg = AppConfig.fromEnv(openAiEnv()).llm
assertEquals(LlmConfig.DEFAULT_SYSTEM_PROMPT, cfg.systemPrompt) assertEquals(LlmConfig.DEFAULT_SYSTEM_PROMPT, cfg.systemPrompt)
} }
@Test @Test
fun `fromEnv — missing base url throws`() { fun `fromEnv - missing base url throws`() {
assertFailsWith<IllegalStateException> { assertFailsWith<IllegalStateException> {
AppConfig.fromEnv { name -> AppConfig.fromEnv { name ->
when (name) { when (name) {
@@ -51,7 +51,7 @@ class LlmConfigTest {
} }
@Test @Test
fun `fromEnv — missing api key throws`() { fun `fromEnv - missing api key throws`() {
assertFailsWith<IllegalStateException> { assertFailsWith<IllegalStateException> {
AppConfig.fromEnv { name -> AppConfig.fromEnv { name ->
when (name) { when (name) {
@@ -64,7 +64,7 @@ class LlmConfigTest {
} }
@Test @Test
fun `fromEnv — missing model throws`() { fun `fromEnv - missing model throws`() {
assertFailsWith<IllegalStateException> { assertFailsWith<IllegalStateException> {
AppConfig.fromEnv { name -> AppConfig.fromEnv { name ->
when (name) { when (name) {
@@ -91,7 +91,7 @@ class LlmConfigTest {
} }
@Test @Test
fun `fromEnv — OPENAI_CONTEXT_WINDOW parsed into OpenAiConfig`() { fun `fromEnv - OPENAI_CONTEXT_WINDOW parsed into OpenAiConfig`() {
val cfg = AppConfig.fromEnv { name -> val cfg = AppConfig.fromEnv { name ->
when (name) { when (name) {
"OPENAI_BASE_URL" -> "https://api.openai.com/v1" "OPENAI_BASE_URL" -> "https://api.openai.com/v1"
@@ -105,7 +105,7 @@ class LlmConfigTest {
} }
@Test @Test
fun `contextWindow — env parsed once and exposed via derived prop`() { fun `contextWindow - env parsed once and exposed via derived prop`() {
// Старый resolveContextWindow проверял "env wins over config" — но в новой // Старый resolveContextWindow проверял "env wins over config" — но в новой
// модели env и config это одно и то же значение (env читается один раз в // модели env и config это одно и то же значение (env читается один раз в
// AppConfig.fromEnv и сохраняется в OpenAiConfig.contextWindow). Поэтому // AppConfig.fromEnv и сохраняется в OpenAiConfig.contextWindow). Поэтому
@@ -124,13 +124,13 @@ class LlmConfigTest {
} }
@Test @Test
fun `contextWindow — returns null when nothing set`() { fun `contextWindow - returns null when nothing set`() {
val cfg = AppConfig.fromEnv(openAiEnv()).llm val cfg = AppConfig.fromEnv(openAiEnv()).llm
assertEquals(null, cfg.contextWindow) assertEquals(null, cfg.contextWindow)
} }
@Test @Test
fun `fromEnv — OPENAI_CONTEXT_WINDOW garbage falls back to null`() { fun `fromEnv - OPENAI_CONTEXT_WINDOW garbage falls back to null`() {
val cfg = AppConfig.fromEnv { name -> val cfg = AppConfig.fromEnv { name ->
when (name) { when (name) {
"OPENAI_BASE_URL" -> "https://api.openai.com/v1" "OPENAI_BASE_URL" -> "https://api.openai.com/v1"
@@ -28,7 +28,7 @@ class PersistenceTest {
} }
@Test @Test
fun `upsert + get conversation — roundtrip`() = runTest { fun `upsert + get conversation - roundtrip`() = runTest {
val now = Instant.fromEpochMilliseconds(1_700_000_000_000) val now = Instant.fromEpochMilliseconds(1_700_000_000_000)
val rec = ConversationRecord( val rec = ConversationRecord(
id = "c1", id = "c1",
@@ -114,7 +114,7 @@ class PersistenceTest {
} }
@Test @Test
fun `message audit log — append and read back`() = runTest { fun `message audit log - append and read back`() = runTest {
val t0 = Instant.fromEpochMilliseconds(1_700_000_000_000) val t0 = Instant.fromEpochMilliseconds(1_700_000_000_000)
stores.messages.append( stores.messages.append(
MessageRecord.UserMessage("m1", "c1", listOf(Content.Text("hi")), t0), MessageRecord.UserMessage("m1", "c1", listOf(Content.Text("hi")), t0),
@@ -147,7 +147,7 @@ class PersistenceTest {
} }
@Test @Test
fun `working memory — append + list preserves order`() = runTest { fun `working memory - append + list preserves order`() = runTest {
val t0 = Instant.fromEpochMilliseconds(1_700_000_000_000) val t0 = Instant.fromEpochMilliseconds(1_700_000_000_000)
val t1 = Instant.fromEpochMilliseconds(1_700_000_001_000) val t1 = Instant.fromEpochMilliseconds(1_700_000_001_000)
stores.workingMemory.append( stores.workingMemory.append(
@@ -167,7 +167,7 @@ class PersistenceTest {
} }
@Test @Test
fun `working memory — compact without summary just drops tail`() = runTest { fun `working memory - compact without summary just drops tail`() = runTest {
val t0 = Instant.fromEpochMilliseconds(1_700_000_000_000) val t0 = Instant.fromEpochMilliseconds(1_700_000_000_000)
stores.workingMemory.append("c1", WorkingMemoryEntry.User("m1", listOf(Content.Text("u1"))), t0) stores.workingMemory.append("c1", WorkingMemoryEntry.User("m1", listOf(Content.Text("u1"))), t0)
stores.workingMemory.append("c1", WorkingMemoryEntry.Assistant("m2", listOf(Content.Text("a1"))), t0) stores.workingMemory.append("c1", WorkingMemoryEntry.Assistant("m2", listOf(Content.Text("a1"))), t0)
@@ -182,7 +182,7 @@ class PersistenceTest {
} }
@Test @Test
fun `working memory — compact with summary inserts Summary entry`() = runTest { fun `working memory - compact with summary inserts Summary entry`() = runTest {
val t0 = Instant.fromEpochMilliseconds(1_700_000_000_000) val t0 = Instant.fromEpochMilliseconds(1_700_000_000_000)
stores.workingMemory.append("c1", WorkingMemoryEntry.User("m1", listOf(Content.Text("u1"))), t0) stores.workingMemory.append("c1", WorkingMemoryEntry.User("m1", listOf(Content.Text("u1"))), t0)
stores.workingMemory.append("c1", WorkingMemoryEntry.Assistant("m2", listOf(Content.Text("a1"))), t0) stores.workingMemory.append("c1", WorkingMemoryEntry.Assistant("m2", listOf(Content.Text("a1"))), t0)
@@ -203,7 +203,7 @@ class PersistenceTest {
} }
@Test @Test
fun `working memory — compact with blank summaryText behaves as drop`() = runTest { fun `working memory - compact with blank summaryText behaves as drop`() = runTest {
val t0 = Instant.fromEpochMilliseconds(1_700_000_000_000) val t0 = Instant.fromEpochMilliseconds(1_700_000_000_000)
stores.workingMemory.append("c1", WorkingMemoryEntry.User("m1", listOf(Content.Text("u1"))), t0) stores.workingMemory.append("c1", WorkingMemoryEntry.User("m1", listOf(Content.Text("u1"))), t0)
val rows = stores.workingMemory.list("c1") val rows = stores.workingMemory.list("c1")
@@ -214,7 +214,7 @@ class PersistenceTest {
} }
@Test @Test
fun `working memory — compact is atomic on other conversations`() = runTest { fun `working memory - compact is atomic on other conversations`() = runTest {
val t0 = Instant.fromEpochMilliseconds(1_700_000_000_000) val t0 = Instant.fromEpochMilliseconds(1_700_000_000_000)
stores.workingMemory.append("c1", WorkingMemoryEntry.User("m1", listOf(Content.Text("u1"))), t0) stores.workingMemory.append("c1", WorkingMemoryEntry.User("m1", listOf(Content.Text("u1"))), t0)
stores.workingMemory.append("c1", WorkingMemoryEntry.Assistant("m2", listOf(Content.Text("a1"))), t0) stores.workingMemory.append("c1", WorkingMemoryEntry.Assistant("m2", listOf(Content.Text("a1"))), t0)