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 <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-23 12:04:15 +02:00
parent 15d00f039c
commit a141e99a07
8 changed files with 91 additions and 22 deletions

View File

@@ -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) {

View File

@@ -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")
);
}
}

View File

@@ -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

View File

@@ -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");
}
}

View File

@@ -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);