Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions .github/workflows/forky-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
name: Release

on:
workflow_dispatch:
inputs:
releaseVersion:
description: 'Version to release'
required: true
jobs:
main:
# We want to keep this repository an official fork
# So this needs to stay public...
# Target only public infra (cloud runners, github packages, etc)
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
java-version: 21
distribution: adopt

- name: Setup node
uses: actions/setup-node@v3
with:
node-version: "22.x"

Comment on lines +22 to +26
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Upgrade to a supported setup-node version.

actions/setup-node@v3 still runs on the deprecated Node16 runtime and is blocked on ubuntu-24.04 runners, so the workflow fails before any build logic runs. Please bump to @v4 (or later).

-      - name: Setup node
-        uses: actions/setup-node@v3
+      - name: Setup node
+        uses: actions/setup-node@v4
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- name: Setup node
uses: actions/setup-node@v3
with:
node-version: "22.x"
- name: Setup node
uses: actions/setup-node@v4
with:
node-version: "22.x"
🧰 Tools
🪛 actionlint (1.7.7)

23-23: the runner of "actions/setup-node@v3" action is too old to run on GitHub Actions. update the action's version to fix this issue

(action)

🤖 Prompt for AI Agents
.github/workflows/forky-release.yml around lines 22-26: actions/setup-node is
pinned to @v3 which uses deprecated Node16 runtime and fails on ubuntu-24.04;
update the action to a supported major by changing the uses line to
actions/setup-node@v4 (or later) and keep or adjust the node-version input
(e.g., "22.x") as needed so the runner uses a supported setup-node
implementation.

- name: Install node modules
run: npm ci

- name: Run get new version
run: npm run release-dry
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: BootJar with version
run: ./gradlew bootJar
env:
VERSION: ${{ github.event.inputs.releaseVersion }}

- name: Prepare for docker build
run: ./gradlew dockerPrepare
env:
VERSION: ${{ github.event.inputs.releaseVersion }}

- name: Create docker image
env:
DOCKER_REGISTRY: "ghcr.io"
IMAGE_PATH: "rakutenfrance/tolgee"
run: |
echo ${{ secrets.GITHUB_TOKEN }} | docker login ${{ env.DOCKER_REGISTRY }} -u $ --password-stdin
docker build . -t ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_PATH }}:${{ github.event.inputs.releaseVersion }} --platform linux/amd64
docker push ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_PATH }}:${{ github.event.inputs.releaseVersion }}
Comment on lines +50 to +52
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix the Docker login username.

-u $ expands to an empty string, so docker login refuses the authentication and the push never happens. Use a real username (e.g. github.actor) and keep the token on stdin.

-          echo ${{ secrets.GITHUB_TOKEN }} | docker login ${{ env.DOCKER_REGISTRY }} -u $ --password-stdin
+          echo "${{ secrets.GITHUB_TOKEN }}" | docker login "${{ env.DOCKER_REGISTRY }}" -u "${{ github.actor }}" --password-stdin
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
echo ${{ secrets.GITHUB_TOKEN }} | docker login ${{ env.DOCKER_REGISTRY }} -u $ --password-stdin
docker build . -t ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_PATH }}:${{ github.event.inputs.releaseVersion }} --platform linux/amd64
docker push ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_PATH }}:${{ github.event.inputs.releaseVersion }}
echo "${{ secrets.GITHUB_TOKEN }}" | docker login "${{ env.DOCKER_REGISTRY }}" -u "${{ github.actor }}" --password-stdin
docker build . -t ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_PATH }}:${{ github.event.inputs.releaseVersion }} --platform linux/amd64
docker push ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_PATH }}:${{ github.event.inputs.releaseVersion }}
🤖 Prompt for AI Agents
.github/workflows/forky-release.yml around lines 50-52: the Docker login uses
`-u $` which expands to an empty username and breaks authentication; replace the
empty username with a real actor variable (e.g. `-u ${{ github.actor }}`) while
continuing to pipe the token via `--password-stdin` so the command becomes: echo
the token | docker login <registry> -u ${{ github.actor }} --password-stdin,
then keep the build and push steps unchanged.

working-directory: build/docker

- name: Pack with webapp
run: ./gradlew packResources
env:
VERSION: ${{ github.event.inputs.releaseVersion }}
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
package io.tolgee.api.v2.controllers

import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.tags.Tag
import io.tolgee.batch.BatchJobChunkExecutionQueue
import io.tolgee.batch.BatchJobProjectLockingManager
import io.tolgee.batch.BatchJobService
import io.tolgee.batch.JobCharacter
import io.tolgee.model.batch.BatchJobStatus
import io.tolgee.openApiDocs.OpenApiSelfHostedExtension
import io.tolgee.security.authentication.RequiresSuperAuthentication
import io.tolgee.util.Logging
import io.tolgee.util.logger
import org.springframework.hateoas.CollectionModel
import org.springframework.web.bind.annotation.CrossOrigin
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController

/**
* REST API for managing project batch job locks
*/
@RestController
@CrossOrigin(origins = ["*"])
@RequestMapping("/v2/administration")
@Tag(
name = "Server Administration",
description = "**Only for self-hosted instances** \n\n" +
"Management of project-level batch job locks and queue inspection for debugging and maintenance."
)
@OpenApiSelfHostedExtension
class ProjectBatchLockController(
private val batchJobProjectLockingManager: BatchJobProjectLockingManager,
private val batchJobService: BatchJobService,
private val batchJobChunkExecutionQueue: BatchJobChunkExecutionQueue,
) : IController, Logging {
Comment on lines +20 to +36
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify presence of PUT/DELETE endpoints used in tests.

Tests call:

  • PUT /v2/administration/project-batch-locks/{id}/clear
  • DELETE /v2/administration/project-batch-locks/{id}

They are not in this controller. Ensure they exist elsewhere or add them here.


🏁 Script executed:

#!/bin/bash
# Search for project-batch-locks admin endpoints in the codebase
rg -nP -C3 --type=kotlin '@(PutMapping|DeleteMapping)\s*\(\s*"/v2/administration/project-batch-locks'
rg -nP -C3 --type=kotlin 'RequestMapping\(.*/v2/administration/project-batch-locks'

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Broad search for any mappings or references to project-batch-locks
rg -nP --type=kotlin '@\w+Mapping\s*\(\s*"/v2/administration/project-batch-locks' -C3
rg -n --type=kotlin 'project-batch-locks' -C3

Length of output: 5387


Add missing PUT and DELETE mappings in ProjectBatchLockController
Tests call PUT /v2/administration/project-batch-locks/{id}/clear and DELETE /v2/administration/project-batch-locks/{id}, but neither handler is implemented in ProjectBatchLockController.kt. Implement these endpoints to satisfy the tests.

🤖 Prompt for AI Agents
In
backend/api/src/main/kotlin/io/tolgee/api/v2/controllers/ProjectBatchLockController.kt
around lines 20-36, add two REST handlers: a PUT at
/v2/administration/project-batch-locks/{id}/clear and a DELETE at
/v2/administration/project-batch-locks/{id}. The PUT handler should accept the
path variable id (project id), call the batchJobProjectLockingManager method
that clears/releases the project lock, and also ensure any related queued chunks
or tracking are cleared via batchJobChunkExecutionQueue and/or batchJobService
as appropriate, then return a 204/OK response. The DELETE handler should accept
the path variable id, remove any persistent batch-job records for that project
via batchJobService and clear related queue entries via
batchJobChunkExecutionQueue and/or batchJobProjectLockingManager, and return an
appropriate 204/OK response. Annotate both methods with
@PutMapping/@DeleteMapping and @PathVariable, handle missing/not-found cases
with suitable HTTP statuses, and add minimal logging consistent with the
controller.


@GetMapping("/project-batch-locks")
@Operation(
summary = "Get all project batch locks",
description = "Returns current project batch job locks from Redis or local storage based on configuration"
)
@RequiresSuperAuthentication
fun getProjectLocks(): CollectionModel<ProjectLockModel> {
logger.debug("Retrieving all project batch locks")

val locks = batchJobProjectLockingManager.getMap()
val lockModels = locks.entries.map { (projectId, lockedJobIds) ->
createProjectLockModel(projectId, lockedJobIds)
}

logger.debug("Retrieved ${lockModels.size} project batch locks")
return CollectionModel.of(lockModels)
}

private fun createProjectLockModel(projectId: Long, lockedJobIds: Set<Long>): ProjectLockModel {
val lockStatus = when {
lockedJobIds.isEmpty() -> LockStatus.UNLOCKED
else -> LockStatus.LOCKED
}

val jobInfos = lockedJobIds.mapNotNull { jobId ->
try {
val jobDto = batchJobService.getJobDto(jobId)
JobInfo(
jobId = jobDto.id,
status = jobDto.status,
type = jobDto.type,
createdAt = jobDto.createdAt
)
} catch (e: Exception) {
logger.warn("Could not retrieve job info for locked job $jobId in project $projectId", e)
null
}
}

return ProjectLockModel(
projectId = projectId,
lockedJobIds = lockedJobIds,
lockStatus = lockStatus,
jobInfos = jobInfos
)
}

@GetMapping("/batch-job-queue")
@Operation(
summary = "Get current batch job queue",
description = "Returns all chunk execution items currently in the batch job queue"
)
@RequiresSuperAuthentication
fun getBatchJobQueue(): CollectionModel<QueueItemModel> {
logger.debug("Retrieving current batch job queue")

val queueItems = batchJobChunkExecutionQueue.getAllQueueItems()
val queueModels = queueItems.map { item ->
QueueItemModel(
chunkExecutionId = item.chunkExecutionId,
jobId = item.jobId,
executeAfter = item.executeAfter,
jobCharacter = item.jobCharacter,
managementErrorRetrials = item.managementErrorRetrials
)
}

logger.debug("Retrieved ${queueModels.size} items from batch job queue")
return CollectionModel.of(queueModels)
}
}

/**
* Model representing a project batch lock
*/
data class ProjectLockModel(
val projectId: Long,
val lockedJobIds: Set<Long>,
val lockStatus: LockStatus,
val jobInfos: List<JobInfo>
)

/**
* Information about the locked job
*/
data class JobInfo(
val jobId: Long,
val status: BatchJobStatus,
val type: io.tolgee.batch.data.BatchJobType,
val createdAt: Long?
)

/**
* Status of the project lock
*/
enum class LockStatus {
/** Project lock is explicitly cleared (value = empty set) */
UNLOCKED,

/** Project lock is held by one or more jobs (value = set of job IDs) */
LOCKED
}

/**
* Model representing a queue item for batch job chunk execution
*/
data class QueueItemModel(
val chunkExecutionId: Long,
val jobId: Long,
val executeAfter: Long?,
val jobCharacter: JobCharacter,
val managementErrorRetrials: Int
)
28 changes: 28 additions & 0 deletions backend/app/src/main/resources/logback-spring.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<springProfile name="!json-logging">
<!-- Default console appender for non-JSON logging -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>

<root level="INFO">
<appender-ref ref="CONSOLE"/>
</root>
</springProfile>

<springProfile name="json-logging">
<!-- JSON console appender using standard logback pattern -->
<appender name="JSON_CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>{"timestamp":"%d{yyyy-MM-dd'T'HH:mm:ss.SSSZ}","level":"%p","logger":"%c","message":"%m","thread":"%t","exception":"%ex{full}"}%n</pattern>
</encoder>
</appender>

<root level="INFO">
<appender-ref ref="JSON_CONSOLE"/>
</root>
Comment on lines +17 to +26
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Use a dedicated JSON encoder instead of a raw pattern.

This pattern layout only resembles JSON; %m, %c, %ex{full} etc. will happily inject quotes, backslashes, and newlines without escaping, so any log containing " or a stack trace breaks consumers expecting valid JSON. Please switch to a real JSON encoder (e.g., ch.qos.logback.contrib.json.classic.JsonLayout, net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder, or Spring Boot’s default LoggingSystemProperties with LOGGING_LEVEL_PATTERN) or add a layout that handles escaping.

🤖 Prompt for AI Agents
In backend/app/src/main/resources/logback-spring.xml around lines 17 to 26, the
ConsoleAppender currently uses a raw pattern string that can produce invalid
JSON when messages or stack traces contain quotes/newlines; replace the
pattern-based encoder with a dedicated JSON encoder (for example use
net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder or
ch.qos.logback.contrib.json.classic.JsonLayout) and configure it to emit
timestamp, level, logger, thread, message and exception fields with proper
escaping/stacktrace handling; remove the pattern block and add the appropriate
encoder/layout element and required dependencies so consumers always receive
well-formed JSON.

</springProfile>
</configuration>
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package io.tolgee.api.v2.controllers

import io.tolgee.batch.BatchJobChunkExecutionQueue
import io.tolgee.batch.BatchJobProjectLockingManager
import io.tolgee.batch.BatchJobService
import io.tolgee.batch.data.ExecutionQueueItem
import io.tolgee.batch.data.BatchJobDto
import io.tolgee.batch.data.BatchJobType
import io.tolgee.development.testDataBuilder.data.AdministrationTestData
import io.tolgee.fixtures.andIsOk
import io.tolgee.fixtures.andIsUnauthorized
import io.tolgee.model.batch.BatchJobStatus
import io.tolgee.testing.AuthorizedControllerTest
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.mockito.kotlin.whenever
import org.springframework.boot.test.mock.mockito.MockBean
import java.util.concurrent.ConcurrentHashMap

class ProjectBatchLockControllerTest : AuthorizedControllerTest() {

@MockBean
private lateinit var batchJobProjectLockingManager: BatchJobProjectLockingManager

@MockBean
private lateinit var batchJobService: BatchJobService

@MockBean
private lateinit var batchJobChunkExecutionQueue: BatchJobChunkExecutionQueue

lateinit var testData: AdministrationTestData

@BeforeEach
fun createData() {
testData = AdministrationTestData()
testDataService.saveTestData(testData.root)
userAccount = testData.admin
}

@Test
fun `GET project-batch-locks returns unauthorized without super auth`() {
// Test without admin user
userAccount = testData.user
performAuthGet("/v2/administration/project-batch-locks")
.andIsUnauthorized
}

@Test
fun `GET project-batch-locks returns locks with super auth`() {
val testLocks = ConcurrentHashMap<Long, Set<Long>>().apply {
put(1L, setOf(123L)) // Project 1 locked to job 123
put(2L, emptySet()) // Project 2 explicitly unlocked
put(3L, emptySet()) // Project 3 no jobs locked
}

whenever(batchJobProjectLockingManager.getMap()).thenReturn(testLocks)

// Mock job info for locked job
val mockJobDto = BatchJobDto(
id = 123L,
projectId = 1L,
authorId = 1L,
target = emptyList(),
totalItems = 100,
totalChunks = 10,
chunkSize = 10,
status = BatchJobStatus.RUNNING,
type = BatchJobType.MACHINE_TRANSLATE,
params = null,
maxPerJobConcurrency = 1,
jobCharacter = io.tolgee.batch.JobCharacter.FAST,
hidden = false,
debouncingKey = null,
createdAt = System.currentTimeMillis()
)

whenever(batchJobService.getJobDto(123L)).thenReturn(mockJobDto)

performAuthGet("/v2/administration/project-batch-locks")
.andIsOk
}

@Test
fun `PUT clear project lock works with super auth`() {
val testLocks = ConcurrentHashMap<Long, Set<Long>>()
whenever(batchJobProjectLockingManager.getMap()).thenReturn(testLocks)

performAuthPut("/v2/administration/project-batch-locks/123/clear", null)
.andIsOk
}

@Test
fun `DELETE project lock works with super auth`() {
val testLocks = ConcurrentHashMap<Long, Set<Long>>().apply {
put(123L, setOf(456L))
}
whenever(batchJobProjectLockingManager.getMap()).thenReturn(testLocks)

performAuthDelete("/v2/administration/project-batch-locks/123")
.andIsOk
}
Comment on lines +83 to +101
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

PUT/DELETE tests assume endpoints exist. Please verify.


🏁 Script executed:

#!/bin/bash
# Verify controller mappings exist for the tested routes
rg -nP --type=kotlin -C3 '@PutMapping\("/v2/administration/project-batch-locks/.*/clear"\)'
rg -nP --type=kotlin -C3 '@DeleteMapping\("/v2/administration/project-batch-locks/.*"\)'

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Find controller files related to project batch locking endpoints
fd -IH 'ProjectBatchLock*Controller*' -e kt

# Search for mapping annotations for project-batch-locks endpoints
rg -n '/v2/administration/project-batch-locks' -g '*.kt'

# Search for usage of batchJobProjectLockingManager in code
rg -n 'batchJobProjectLockingManager' -g '*.kt'

Length of output: 3796


🏁 Script executed:

#!/bin/bash
rg -n '@PutMapping' -C3 backend/api/src/main/kotlin/io/tolgee/api/v2/controllers/ProjectBatchLockController.kt
rg -n '@DeleteMapping' -C3 backend/api/src/main/kotlin/io/tolgee/api/v2/controllers/ProjectBatchLockController.kt

Length of output: 0


🏁 Script executed:

#!/bin/bash
rg -nE '@(RequestMapping|GetMapping|PutMapping|DeleteMapping)' -C2 backend/api/src/main/kotlin/io/tolgee/api/v2/controllers/ProjectBatchLockController.kt

Length of output: 90


🏁 Script executed:

#!/bin/bash
# Print the beginning of the controller to inspect class-level mapping
sed -n '1,200p' backend/api/src/main/kotlin/io/tolgee/api/v2/controllers/ProjectBatchLockController.kt

# Look for class-level RequestMapping
rg -n '@RequestMapping' -C2 backend/api/src/main/kotlin/io/tolgee/api/v2/controllers/ProjectBatchLockController.kt

# Search for any 'clear' usage
rg -n 'clear' -C3 backend/api/src/main/kotlin/io/tolgee/api/v2/controllers/ProjectBatchLockController.kt

# Search for HTTP delete handler
rg -n 'fun .*delete' -C2 backend/api/src/main/kotlin/io/tolgee/api/v2/controllers/ProjectBatchLockController.kt

Length of output: 5018


Implement missing PUT/DELETE endpoints for project batch locks
ProjectBatchLockController.kt currently only defines GET mappings; the tests for PUT /v2/administration/project-batch-locks/{id}/clear and DELETE /v2/administration/project-batch-locks/{id} will fail. Add corresponding @PutMapping and @DeleteMapping methods (with @RequiresSuperAuthentication) to handle “clear” and “delete” operations.

🤖 Prompt for AI Agents
In
backend/app/src/main/kotlin/io/tolgee/api/v2/controllers/ProjectBatchLockController.kt
around where GET mappings are defined (add new methods corresponding to tests
roughly next to existing endpoints), implement two new endpoints: add a
@PutMapping("/v2/administration/project-batch-locks/{id}/clear") method
annotated with @RequiresSuperAuthentication that reads the id path variable,
retrieves batchJobProjectLockingManager.getMap(), and if an entry exists clears
the Set for that id (e.g. replace with emptySet or clear the collection) and
returns an OK response; add a
@DeleteMapping("/v2/administration/project-batch-locks/{id}") method annotated
with @RequiresSuperAuthentication that removes the map entry for the given id
via batchJobProjectLockingManager.getMap().remove(id) and returns an OK
response; ensure proper path variable types and return types match other
controller methods (e.g. ResponseEntity or Unit mapped to 200) and include
imports/annotations as needed.


@Test
fun `GET batch-job-queue returns queue items with super auth`() {
val queueItems = listOf(
ExecutionQueueItem(
chunkExecutionId = 1001L,
jobId = 2001L,
executeAfter = System.currentTimeMillis(),
jobCharacter = io.tolgee.batch.JobCharacter.FAST,
managementErrorRetrials = 0
),
ExecutionQueueItem(
chunkExecutionId = 1002L,
jobId = 2002L,
executeAfter = null,
jobCharacter = io.tolgee.batch.JobCharacter.SLOW,
managementErrorRetrials = 1
)
)

whenever(batchJobChunkExecutionQueue.getAllQueueItems()).thenReturn(queueItems)

performAuthGet("/v2/administration/batch-job-queue")
.andIsOk
}

@Test
fun `GET batch-job-queue returns unauthorized without super auth`() {
// Test without admin user
userAccount = testData.user
performAuthGet("/v2/administration/batch-job-queue")
.andIsUnauthorized
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -409,7 +409,7 @@ class BatchJobTestUtil(

fun verifyProjectJobLockReleased() {
waitFor(pollTime = 200, timeout = 1000) {
batchJobProjectLockingManager.getLockedForProject(testData.projectBuilder.self.id) == 0L
batchJobProjectLockingManager.getLockedForProject(testData.projectBuilder.self.id).isEmpty()
}
}

Expand Down
Loading
Loading