ci: UTF-8 локаль + ASCII-дефис в именах тестов; release через общий publish action
ci / JVM build + tests (push) Failing after 2m44s
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:
@@ -17,6 +17,14 @@ concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
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:
|
||||
build-jvm:
|
||||
name: JVM build + tests
|
||||
|
||||
@@ -8,10 +8,13 @@
|
||||
# :<module>:shadowJar`) и загружается в релиз через Gitea UI / API
|
||||
# отдельно от этого workflow.
|
||||
#
|
||||
# Требуемые Gitea Action Variables:
|
||||
# BINOM_REPO_URL — например http://192.168.76.117/repository/caffeine/
|
||||
# Требуемые Gitea Action Secrets:
|
||||
# BINOM_REPO_USER, BINOM_REPO_PASSWORD — креды Nexus с правами на публикацию.
|
||||
# Версия публикации = имя тега релиза (без префикса 'v'). Релиз с именем "3"
|
||||
# публикует pw.binom.agentik:*:3 в 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
|
||||
|
||||
on:
|
||||
@@ -22,6 +25,15 @@ concurrency:
|
||||
group: release-${{ github.ref }}
|
||||
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:
|
||||
publish-libraries:
|
||||
name: Publish KMP libraries → caffeine Nexus
|
||||
@@ -31,46 +43,7 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup JDK 21
|
||||
uses: actions/setup-java@v4
|
||||
- name: Publish libraries (all KMP targets, all modules) to Nexus
|
||||
uses: https://git.binom.pw/subochev/devops/publish@main
|
||||
with:
|
||||
java-version: '21'
|
||||
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
|
||||
version: ${{ gitea.ref_name }}
|
||||
|
||||
@@ -205,7 +205,7 @@ class TuiBackendTest {
|
||||
}
|
||||
|
||||
@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 state = AppState(cfg)
|
||||
val agent = FakeAgent(conversationFactory = { error("server kaboom") })
|
||||
|
||||
@@ -555,7 +555,7 @@ class ChatAgentTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `agentEvents — Created + Deleted flow`() = runTest {
|
||||
fun `agentEvents - Created + Deleted flow`() = runTest {
|
||||
val agent = newAgent()
|
||||
val events = mutableListOf<AgentEvent>()
|
||||
val job = launch(start = kotlinx.coroutines.CoroutineStart.UNDISPATCHED) {
|
||||
|
||||
@@ -17,7 +17,7 @@ class LlmConfigTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fromEnv — happy path`() {
|
||||
fun `fromEnv - happy path`() {
|
||||
val cfg = AppConfig.fromEnv { name ->
|
||||
when (name) {
|
||||
"OPENAI_BASE_URL" -> "https://api.openai.com/v1"
|
||||
@@ -32,13 +32,13 @@ class LlmConfigTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fromEnv — falls back to default system prompt`() {
|
||||
fun `fromEnv - falls back to default system prompt`() {
|
||||
val cfg = AppConfig.fromEnv(openAiEnv()).llm
|
||||
assertEquals(LlmConfig.DEFAULT_SYSTEM_PROMPT, cfg.systemPrompt)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fromEnv — missing base url throws`() {
|
||||
fun `fromEnv - missing base url throws`() {
|
||||
assertFailsWith<IllegalStateException> {
|
||||
AppConfig.fromEnv { name ->
|
||||
when (name) {
|
||||
@@ -51,7 +51,7 @@ class LlmConfigTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fromEnv — missing api key throws`() {
|
||||
fun `fromEnv - missing api key throws`() {
|
||||
assertFailsWith<IllegalStateException> {
|
||||
AppConfig.fromEnv { name ->
|
||||
when (name) {
|
||||
@@ -64,7 +64,7 @@ class LlmConfigTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fromEnv — missing model throws`() {
|
||||
fun `fromEnv - missing model throws`() {
|
||||
assertFailsWith<IllegalStateException> {
|
||||
AppConfig.fromEnv { name ->
|
||||
when (name) {
|
||||
@@ -91,7 +91,7 @@ class LlmConfigTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fromEnv — OPENAI_CONTEXT_WINDOW parsed into OpenAiConfig`() {
|
||||
fun `fromEnv - OPENAI_CONTEXT_WINDOW parsed into OpenAiConfig`() {
|
||||
val cfg = AppConfig.fromEnv { name ->
|
||||
when (name) {
|
||||
"OPENAI_BASE_URL" -> "https://api.openai.com/v1"
|
||||
@@ -105,7 +105,7 @@ class LlmConfigTest {
|
||||
}
|
||||
|
||||
@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" — но в новой
|
||||
// модели env и config это одно и то же значение (env читается один раз в
|
||||
// AppConfig.fromEnv и сохраняется в OpenAiConfig.contextWindow). Поэтому
|
||||
@@ -124,13 +124,13 @@ class LlmConfigTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `contextWindow — returns null when nothing set`() {
|
||||
fun `contextWindow - returns null when nothing set`() {
|
||||
val cfg = AppConfig.fromEnv(openAiEnv()).llm
|
||||
assertEquals(null, cfg.contextWindow)
|
||||
}
|
||||
|
||||
@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 ->
|
||||
when (name) {
|
||||
"OPENAI_BASE_URL" -> "https://api.openai.com/v1"
|
||||
|
||||
@@ -28,7 +28,7 @@ class PersistenceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `upsert + get conversation — roundtrip`() = runTest {
|
||||
fun `upsert + get conversation - roundtrip`() = runTest {
|
||||
val now = Instant.fromEpochMilliseconds(1_700_000_000_000)
|
||||
val rec = ConversationRecord(
|
||||
id = "c1",
|
||||
@@ -114,7 +114,7 @@ class PersistenceTest {
|
||||
}
|
||||
|
||||
@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)
|
||||
stores.messages.append(
|
||||
MessageRecord.UserMessage("m1", "c1", listOf(Content.Text("hi")), t0),
|
||||
@@ -147,7 +147,7 @@ class PersistenceTest {
|
||||
}
|
||||
|
||||
@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 t1 = Instant.fromEpochMilliseconds(1_700_000_001_000)
|
||||
stores.workingMemory.append(
|
||||
@@ -167,7 +167,7 @@ class PersistenceTest {
|
||||
}
|
||||
|
||||
@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)
|
||||
stores.workingMemory.append("c1", WorkingMemoryEntry.User("m1", listOf(Content.Text("u1"))), t0)
|
||||
stores.workingMemory.append("c1", WorkingMemoryEntry.Assistant("m2", listOf(Content.Text("a1"))), t0)
|
||||
@@ -182,7 +182,7 @@ class PersistenceTest {
|
||||
}
|
||||
|
||||
@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)
|
||||
stores.workingMemory.append("c1", WorkingMemoryEntry.User("m1", listOf(Content.Text("u1"))), t0)
|
||||
stores.workingMemory.append("c1", WorkingMemoryEntry.Assistant("m2", listOf(Content.Text("a1"))), t0)
|
||||
@@ -203,7 +203,7 @@ class PersistenceTest {
|
||||
}
|
||||
|
||||
@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)
|
||||
stores.workingMemory.append("c1", WorkingMemoryEntry.User("m1", listOf(Content.Text("u1"))), t0)
|
||||
val rows = stores.workingMemory.list("c1")
|
||||
@@ -214,7 +214,7 @@ class PersistenceTest {
|
||||
}
|
||||
|
||||
@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)
|
||||
stores.workingMemory.append("c1", WorkingMemoryEntry.User("m1", listOf(Content.Text("u1"))), t0)
|
||||
stores.workingMemory.append("c1", WorkingMemoryEntry.Assistant("m2", listOf(Content.Text("a1"))), t0)
|
||||
|
||||
Reference in New Issue
Block a user