From a141e99a07567c5953901156cf4c5753f1d98621 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Thu, 23 Apr 2026 12:04:15 +0200 Subject: [PATCH] feat(deploy): cascade createdBy through Deployment record + service + repo Appends String createdBy to the Deployment record (after createdAt), updates both with-er methods to pass it through, threads the parameter through DeploymentRepository.create, DeploymentService.createDeployment/promote, and PostgresDeploymentRepository (INSERT + SELECT_COLS + mapRow). DeploymentController passes null as placeholder (Task 4 will resolve from SecurityContextHolder). Covers with PostgresDeploymentRepositoryCreatedByIT verifying round-trip via both createDeployment and promote. Co-Authored-By: Claude Sonnet 4.6 --- .../app/controller/DeploymentController.java | 4 +- .../storage/PostgresDeploymentRepository.java | 11 +-- .../eval/DeploymentStateEvaluatorTest.java | 2 +- ...stgresDeploymentRepositoryCreatedByIT.java | 67 +++++++++++++++++++ .../PostgresDeploymentRepositoryIT.java | 12 ++-- .../server/core/runtime/Deployment.java | 7 +- .../core/runtime/DeploymentRepository.java | 2 +- .../core/runtime/DeploymentService.java | 8 +-- 8 files changed, 91 insertions(+), 22 deletions(-) create mode 100644 cameleer-server-app/src/test/java/com/cameleer/server/app/storage/PostgresDeploymentRepositoryCreatedByIT.java diff --git a/cameleer-server-app/src/main/java/com/cameleer/server/app/controller/DeploymentController.java b/cameleer-server-app/src/main/java/com/cameleer/server/app/controller/DeploymentController.java index 2a74d809..be86e9ee 100644 --- a/cameleer-server-app/src/main/java/com/cameleer/server/app/controller/DeploymentController.java +++ b/cameleer-server-app/src/main/java/com/cameleer/server/app/controller/DeploymentController.java @@ -89,7 +89,7 @@ public class DeploymentController { @RequestBody DeployRequest request) { try { App app = appService.getByEnvironmentAndSlug(env.id(), appSlug); - Deployment deployment = deploymentService.createDeployment(app.id(), request.appVersionId(), env.id()); + Deployment deployment = deploymentService.createDeployment(app.id(), request.appVersionId(), env.id(), null); deploymentExecutor.executeAsync(deployment); return ResponseEntity.accepted().body(deployment); } catch (IllegalArgumentException e) { @@ -129,7 +129,7 @@ public class DeploymentController { Environment targetEnv = environmentService.getBySlug(request.targetEnvironment()); // Target must also have the app with the same slug App targetApp = appService.getByEnvironmentAndSlug(targetEnv.id(), appSlug); - Deployment promoted = deploymentService.promote(targetApp.id(), source.appVersionId(), targetEnv.id()); + Deployment promoted = deploymentService.promote(targetApp.id(), source.appVersionId(), targetEnv.id(), null); deploymentExecutor.executeAsync(promoted); return ResponseEntity.accepted().body(promoted); } catch (IllegalArgumentException e) { diff --git a/cameleer-server-app/src/main/java/com/cameleer/server/app/storage/PostgresDeploymentRepository.java b/cameleer-server-app/src/main/java/com/cameleer/server/app/storage/PostgresDeploymentRepository.java index ac9cf512..c1737d30 100644 --- a/cameleer-server-app/src/main/java/com/cameleer/server/app/storage/PostgresDeploymentRepository.java +++ b/cameleer-server-app/src/main/java/com/cameleer/server/app/storage/PostgresDeploymentRepository.java @@ -22,7 +22,7 @@ public class PostgresDeploymentRepository implements DeploymentRepository { private static final String SELECT_COLS = "id, app_id, app_version_id, environment_id, status, target_state, deployment_strategy, " + "replica_states, deploy_stage, container_id, container_name, error_message, " + - "resolved_config, deployed_config_snapshot, deployed_at, stopped_at, created_at"; + "resolved_config, deployed_config_snapshot, deployed_at, stopped_at, created_at, created_by"; private final JdbcTemplate jdbc; private final ObjectMapper objectMapper; @@ -81,10 +81,10 @@ public class PostgresDeploymentRepository implements DeploymentRepository { } @Override - public UUID create(UUID appId, UUID appVersionId, UUID environmentId, String containerName) { + public UUID create(UUID appId, UUID appVersionId, UUID environmentId, String containerName, String createdBy) { UUID id = UUID.randomUUID(); - jdbc.update("INSERT INTO deployments (id, app_id, app_version_id, environment_id, container_name) VALUES (?, ?, ?, ?, ?)", - id, appId, appVersionId, environmentId, containerName); + jdbc.update("INSERT INTO deployments (id, app_id, app_version_id, environment_id, container_name, created_by) VALUES (?, ?, ?, ?, ?, ?)", + id, appId, appVersionId, environmentId, containerName, createdBy); return id; } @@ -216,7 +216,8 @@ public class PostgresDeploymentRepository implements DeploymentRepository { deployedConfigSnapshot, deployedAt != null ? deployedAt.toInstant() : null, stoppedAt != null ? stoppedAt.toInstant() : null, - rs.getTimestamp("created_at").toInstant() + rs.getTimestamp("created_at").toInstant(), + rs.getString("created_by") ); } } diff --git a/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/eval/DeploymentStateEvaluatorTest.java b/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/eval/DeploymentStateEvaluatorTest.java index 06424525..7a9631b8 100644 --- a/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/eval/DeploymentStateEvaluatorTest.java +++ b/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/eval/DeploymentStateEvaluatorTest.java @@ -48,7 +48,7 @@ class DeploymentStateEvaluatorTest { private Deployment deployment(DeploymentStatus status) { return new Deployment(DEP_ID, APP_ID, UUID.randomUUID(), ENV_ID, status, null, null, List.of(), null, null, "orders-0", null, - Map.of(), null, NOW.minusSeconds(60), null, NOW.minusSeconds(120)); + Map.of(), null, NOW.minusSeconds(60), null, NOW.minusSeconds(120), "test-user"); } @Test diff --git a/cameleer-server-app/src/test/java/com/cameleer/server/app/storage/PostgresDeploymentRepositoryCreatedByIT.java b/cameleer-server-app/src/test/java/com/cameleer/server/app/storage/PostgresDeploymentRepositoryCreatedByIT.java new file mode 100644 index 00000000..4bab08fb --- /dev/null +++ b/cameleer-server-app/src/test/java/com/cameleer/server/app/storage/PostgresDeploymentRepositoryCreatedByIT.java @@ -0,0 +1,67 @@ +package com.cameleer.server.app.storage; + +import com.cameleer.server.app.AbstractPostgresIT; +import com.cameleer.server.core.runtime.Deployment; +import com.cameleer.server.core.runtime.DeploymentService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jdbc.core.JdbcTemplate; + +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; + +class PostgresDeploymentRepositoryCreatedByIT extends AbstractPostgresIT { + + @Autowired DeploymentService deploymentService; + @Autowired JdbcTemplate jdbc; + + private UUID appId; + private UUID envId; + private UUID versionId; + + @BeforeEach + void seedAppAndVersion() { + // Clean up to avoid conflicts across test runs + jdbc.update("DELETE FROM deployments"); + jdbc.update("DELETE FROM app_versions"); + jdbc.update("DELETE FROM apps"); + + envId = jdbc.queryForObject( + "SELECT id FROM environments WHERE slug = 'default'", UUID.class); + + // Seed users (alice, bob) — use the bare user_id convention; provider is NOT NULL + jdbc.update("INSERT INTO users (user_id, provider) VALUES (?, 'LOCAL') " + + "ON CONFLICT (user_id) DO NOTHING", "alice"); + jdbc.update("INSERT INTO users (user_id, provider) VALUES (?, 'LOCAL') " + + "ON CONFLICT (user_id) DO NOTHING", "bob"); + + // Seed app + appId = UUID.randomUUID(); + jdbc.update("INSERT INTO apps (id, environment_id, slug, display_name) " + + "VALUES (?, ?, 'test-app', 'Test App')", + appId, envId); + + // Seed version + versionId = UUID.randomUUID(); + jdbc.update("INSERT INTO app_versions (id, app_id, version, jar_path, jar_checksum) " + + "VALUES (?, ?, 1, '/tmp/x.jar', 'abc')", + versionId, appId); + } + + @Test + void createDeployment_persists_createdBy_and_returns_it() { + Deployment d = deploymentService.createDeployment(appId, versionId, envId, "alice"); + assertThat(d.createdBy()).isEqualTo("alice"); + String fromDb = jdbc.queryForObject( + "SELECT created_by FROM deployments WHERE id = ?", String.class, d.id()); + assertThat(fromDb).isEqualTo("alice"); + } + + @Test + void promote_persists_createdBy() { + Deployment promoted = deploymentService.promote(appId, versionId, envId, "bob"); + assertThat(promoted.createdBy()).isEqualTo("bob"); + } +} diff --git a/cameleer-server-app/src/test/java/com/cameleer/server/app/storage/PostgresDeploymentRepositoryIT.java b/cameleer-server-app/src/test/java/com/cameleer/server/app/storage/PostgresDeploymentRepositoryIT.java index 0863256b..d0b6638d 100644 --- a/cameleer-server-app/src/test/java/com/cameleer/server/app/storage/PostgresDeploymentRepositoryIT.java +++ b/cameleer-server-app/src/test/java/com/cameleer/server/app/storage/PostgresDeploymentRepositoryIT.java @@ -65,7 +65,7 @@ class PostgresDeploymentRepositoryIT extends AbstractPostgresIT { null ); - UUID deploymentId = repository.create(appId, appVersionId, envId, "test-container"); + UUID deploymentId = repository.create(appId, appVersionId, envId, "test-container", null); repository.saveDeployedConfigSnapshot(deploymentId, snapshot); // when — load it back @@ -80,7 +80,7 @@ class PostgresDeploymentRepositoryIT extends AbstractPostgresIT { @Test void deployedConfigSnapshot_nullByDefault() { // deployments created without a snapshot must return null (not throw) - UUID deploymentId = repository.create(appId, appVersionId, envId, "test-container-null"); + UUID deploymentId = repository.create(appId, appVersionId, envId, "test-container-null", null); Deployment loaded = repository.findById(deploymentId).orElseThrow(); @@ -90,13 +90,13 @@ class PostgresDeploymentRepositoryIT extends AbstractPostgresIT { @Test void deleteFailedByAppAndEnvironment_keepsStoppedAndActive() { // given: one STOPPED (checkpoint), one FAILED, one RUNNING - UUID stoppedId = repository.create(appId, appVersionId, envId, "stopped"); + UUID stoppedId = repository.create(appId, appVersionId, envId, "stopped", null); repository.updateStatus(stoppedId, com.cameleer.server.core.runtime.DeploymentStatus.STOPPED, null, null); - UUID failedId = repository.create(appId, appVersionId, envId, "failed"); + UUID failedId = repository.create(appId, appVersionId, envId, "failed", null); repository.updateStatus(failedId, com.cameleer.server.core.runtime.DeploymentStatus.FAILED, null, "boom"); - UUID runningId = repository.create(appId, appVersionId, envId, "running"); + UUID runningId = repository.create(appId, appVersionId, envId, "running", null); repository.updateStatus(runningId, com.cameleer.server.core.runtime.DeploymentStatus.RUNNING, "c1", null); // when @@ -118,7 +118,7 @@ class PostgresDeploymentRepositoryIT extends AbstractPostgresIT { null ); - UUID deploymentId = repository.create(appId, appVersionId, envId, "test-container-clear"); + UUID deploymentId = repository.create(appId, appVersionId, envId, "test-container-clear", null); repository.saveDeployedConfigSnapshot(deploymentId, snapshot); repository.saveDeployedConfigSnapshot(deploymentId, null); diff --git a/cameleer-server-core/src/main/java/com/cameleer/server/core/runtime/Deployment.java b/cameleer-server-core/src/main/java/com/cameleer/server/core/runtime/Deployment.java index 374262d7..ff374f68 100644 --- a/cameleer-server-core/src/main/java/com/cameleer/server/core/runtime/Deployment.java +++ b/cameleer-server-core/src/main/java/com/cameleer/server/core/runtime/Deployment.java @@ -22,19 +22,20 @@ public record Deployment( DeploymentConfigSnapshot deployedConfigSnapshot, Instant deployedAt, Instant stoppedAt, - Instant createdAt + Instant createdAt, + String createdBy ) { public Deployment withStatus(DeploymentStatus newStatus) { return new Deployment(id, appId, appVersionId, environmentId, newStatus, targetState, deploymentStrategy, replicaStates, deployStage, containerId, containerName, errorMessage, resolvedConfig, - deployedConfigSnapshot, deployedAt, stoppedAt, createdAt); + deployedConfigSnapshot, deployedAt, stoppedAt, createdAt, createdBy); } public Deployment withDeployedConfigSnapshot(DeploymentConfigSnapshot snapshot) { return new Deployment(id, appId, appVersionId, environmentId, status, targetState, deploymentStrategy, replicaStates, deployStage, containerId, containerName, errorMessage, resolvedConfig, - snapshot, deployedAt, stoppedAt, createdAt); + snapshot, deployedAt, stoppedAt, createdAt, createdBy); } } diff --git a/cameleer-server-core/src/main/java/com/cameleer/server/core/runtime/DeploymentRepository.java b/cameleer-server-core/src/main/java/com/cameleer/server/core/runtime/DeploymentRepository.java index bcb01f0e..292b8765 100644 --- a/cameleer-server-core/src/main/java/com/cameleer/server/core/runtime/DeploymentRepository.java +++ b/cameleer-server-core/src/main/java/com/cameleer/server/core/runtime/DeploymentRepository.java @@ -10,7 +10,7 @@ public interface DeploymentRepository { Optional findById(UUID id); Optional findActiveByAppIdAndEnvironmentId(UUID appId, UUID environmentId); Optional findActiveByAppIdAndEnvironmentIdExcluding(UUID appId, UUID environmentId, UUID excludeDeploymentId); - UUID create(UUID appId, UUID appVersionId, UUID environmentId, String containerName); + UUID create(UUID appId, UUID appVersionId, UUID environmentId, String containerName, String createdBy); void updateStatus(UUID id, DeploymentStatus status, String containerId, String errorMessage); void markDeployed(UUID id); void markStopped(UUID id); diff --git a/cameleer-server-core/src/main/java/com/cameleer/server/core/runtime/DeploymentService.java b/cameleer-server-core/src/main/java/com/cameleer/server/core/runtime/DeploymentService.java index 474bba43..f3371186 100644 --- a/cameleer-server-core/src/main/java/com/cameleer/server/core/runtime/DeploymentService.java +++ b/cameleer-server-core/src/main/java/com/cameleer/server/core/runtime/DeploymentService.java @@ -23,19 +23,19 @@ public class DeploymentService { public Deployment getById(UUID id) { return deployRepo.findById(id).orElseThrow(() -> new IllegalArgumentException("Deployment not found: " + id)); } /** Create a deployment record. Actual container start is handled by DeploymentExecutor (async). */ - public Deployment createDeployment(UUID appId, UUID appVersionId, UUID environmentId) { + public Deployment createDeployment(UUID appId, UUID appVersionId, UUID environmentId, String createdBy) { App app = appService.getById(appId); Environment env = envService.getById(environmentId); String containerName = env.slug() + "-" + app.slug(); deployRepo.deleteFailedByAppAndEnvironment(appId, environmentId); - UUID deploymentId = deployRepo.create(appId, appVersionId, environmentId, containerName); + UUID deploymentId = deployRepo.create(appId, appVersionId, environmentId, containerName, createdBy); return deployRepo.findById(deploymentId).orElseThrow(); } /** Promote: deploy the same app version to a different environment. */ - public Deployment promote(UUID appId, UUID appVersionId, UUID targetEnvironmentId) { - return createDeployment(appId, appVersionId, targetEnvironmentId); + public Deployment promote(UUID appId, UUID appVersionId, UUID targetEnvironmentId, String createdBy) { + return createDeployment(appId, appVersionId, targetEnvironmentId, createdBy); } public void markRunning(UUID deploymentId, String containerId) {