Skip to content

Conversation

@davideme
Copy link
Owner

Add support for three distinct operation modes across all 6 language implementations: serve (default, run migrations then start server), migrate (run migrations only), and serve-only (start server without migrations).

This enables better control over database migrations in production environments, supporting patterns like init containers, separate migration jobs, and zero-downtime deployments.

Changes by implementation:

Go:

  • Add --mode flag with serve/migrate/serve-only options
  • Refactor main.go with runMigrationsOnly() and initializeRepository()
  • Migrations run conditionally based on mode

Kotlin:

  • Add --mode argument parsing in Application.kt
  • Add runMigrationsOnly() for migration-only mode
  • Use System property to skip migrations in serve-only mode
  • Update DatabaseFactory to check skip.migrations property

Java (Spring Boot):

  • Create ApplicationMode class for mode handling
  • Add mode parsing and configuration in main method
  • Control spring.flyway.enabled based on mode
  • Separate runMigrationsOnly() method for migrate mode

Python (FastAPI):

  • Create new cli.py module for CLI handling
  • Add argparse-based mode selection
  • Implement run_migrations_only() using Alembic
  • Conditionally run migrations in start_server()

TypeScript (Fastify):

  • Create new cli.ts entry point
  • Add --mode argument parsing
  • Use execSync to run Prisma migrations
  • Separate functions for migrate-only and server modes

C# (.NET):

  • Update Program.cs with mode argument parsing
  • Add RunMigrationsOnly() helper method
  • Run EF Core migrations conditionally based on mode
  • Use Database.Migrate() for migration execution

Documentation:

  • Add comprehensive OPERATION_MODES.md documentation
  • Include usage examples for all 6 implementations
  • Document production deployment patterns (init containers, jobs, CI/CD)
  • Add troubleshooting guide and environment variables reference

This brings all implementations to feature parity for production deployment scenarios where migrations need to be managed separately from application startup.

Add support for three distinct operation modes across all 6 language
implementations: serve (default, run migrations then start server),
migrate (run migrations only), and serve-only (start server without migrations).

This enables better control over database migrations in production environments,
supporting patterns like init containers, separate migration jobs, and
zero-downtime deployments.

Changes by implementation:

**Go:**
- Add --mode flag with serve/migrate/serve-only options
- Refactor main.go with runMigrationsOnly() and initializeRepository()
- Migrations run conditionally based on mode

**Kotlin:**
- Add --mode argument parsing in Application.kt
- Add runMigrationsOnly() for migration-only mode
- Use System property to skip migrations in serve-only mode
- Update DatabaseFactory to check skip.migrations property

**Java (Spring Boot):**
- Create ApplicationMode class for mode handling
- Add mode parsing and configuration in main method
- Control spring.flyway.enabled based on mode
- Separate runMigrationsOnly() method for migrate mode

**Python (FastAPI):**
- Create new cli.py module for CLI handling
- Add argparse-based mode selection
- Implement run_migrations_only() using Alembic
- Conditionally run migrations in start_server()

**TypeScript (Fastify):**
- Create new cli.ts entry point
- Add --mode argument parsing
- Use execSync to run Prisma migrations
- Separate functions for migrate-only and server modes

**C# (.NET):**
- Update Program.cs with mode argument parsing
- Add RunMigrationsOnly() helper method
- Run EF Core migrations conditionally based on mode
- Use Database.Migrate() for migration execution

**Documentation:**
- Add comprehensive OPERATION_MODES.md documentation
- Include usage examples for all 6 implementations
- Document production deployment patterns (init containers, jobs, CI/CD)
- Add troubleshooting guide and environment variables reference

This brings all implementations to feature parity for production deployment
scenarios where migrations need to be managed separately from application startup.
Copilot AI review requested due to automatic review settings January 17, 2026 13:52
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adds support for three distinct operation modes (serve, migrate, serve-only) across all six language implementations of the Lamp Control API. This enables better control over database migrations in production environments, supporting patterns like init containers, separate migration jobs, and zero-downtime deployments.

Changes:

  • Adds CLI/argument parsing for mode selection in all implementations (Go, Kotlin, Java, Python, TypeScript, C#)
  • Implements separate migration-only and server-only execution paths
  • Refactors migration execution to be conditional based on the selected mode
  • Provides comprehensive documentation with usage examples and deployment patterns

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
src/typescript/src/cli.ts New CLI entry point with mode argument parsing and migration execution via Prisma
src/python/src/openapi_server/cli.py New CLI module with argparse-based mode selection and Alembic integration
src/kotlin/src/main/kotlin/com/lampcontrol/Application.kt Refactored main function with mode parsing and system property-based migration control
src/kotlin/src/main/kotlin/com/lampcontrol/database/DatabaseFactory.kt Added skip.migrations system property check to conditionally skip migrations
src/java/src/main/java/org/openapitools/ApplicationMode.java New class handling mode parsing, configuration, and migration-only execution
src/java/src/main/java/org/openapitools/OpenApiGeneratorApplication.java Updated to use ApplicationMode for mode handling before starting Spring Boot
src/go/cmd/lamp-control-api/main.go Refactored with mode flag, extracted runMigrationsOnly and initializeRepository functions
src/csharp/LampControlApi/Program.cs Added mode argument parsing and RunMigrationsOnly helper method for EF Core migrations
docs/OPERATION_MODES.md Comprehensive documentation covering all implementations, usage examples, and deployment patterns

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 128 to 144
// Handle migrate-only mode
if *mode == "migrate" {
runMigrationsOnly(*requireDB)
return
}

ctx := context.Background()

swagger, err := api.GetSwagger()
if err != nil {
fmt.Fprintf(os.Stderr, "Error loading swagger spec\n: %s", err)
os.Exit(1)
}

// Initialize repository based on mode
runMigrations := *mode == "serve" // Only run migrations in default 'serve' mode
lampAPI, pool := initializeRepository(ctx, runMigrations, *requireDB)
Copy link

Copilot AI Jan 17, 2026

Choose a reason for hiding this comment

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

The mode parameter is not validated before use. If an invalid mode like "invalid-mode" is provided, the code will silently treat it as "serve-only" mode (since runMigrations will be false). This could lead to unexpected behavior where migrations don't run when expected.

Add validation after parsing the mode flag to ensure only valid modes (serve, migrate, serve-only) are accepted, and exit with an error message for invalid values. This would provide consistency with the other implementations (Kotlin, Java, Python, TypeScript) which all validate the mode and exit with an error for invalid values.

Copilot uses AI. Check for mistakes.
Comment on lines +36 to +76
alembic_ini = Path(__file__).parent.parent.parent / "alembic.ini"
if not alembic_ini.exists():
logger.error(f"alembic.ini not found at {alembic_ini}")
sys.exit(1)

# Create Alembic config
alembic_cfg = Config(str(alembic_ini))
alembic_cfg.set_main_option("sqlalchemy.url", settings.database_url)

# Run migrations
command.upgrade(alembic_cfg, "head")

logger.info("Migrations completed successfully")

except Exception as e:
logger.error(f"Migration failed: {e}")
sys.exit(1)


def start_server(run_migrations: bool = True):
"""Start the FastAPI server.
Args:
run_migrations: Whether to run migrations before starting the server
"""
if run_migrations:
logger.info("Starting server with automatic migrations...")
settings = DatabaseSettings()
if settings.use_postgres():
try:
alembic_ini = Path(__file__).parent.parent.parent / "alembic.ini"
if alembic_ini.exists():
alembic_cfg = Config(str(alembic_ini))
alembic_cfg.set_main_option("sqlalchemy.url", settings.database_url)
command.upgrade(alembic_cfg, "head")
logger.info("Migrations completed")
else:
logger.warning(f"alembic.ini not found at {alembic_ini}, skipping migrations")
except Exception as e:
logger.error(f"Migration failed: {e}")
sys.exit(1)
Copy link

Copilot AI Jan 17, 2026

Choose a reason for hiding this comment

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

The migration logic is duplicated between run_migrations_only (lines 36-46) and start_server (lines 66-70) functions. Both functions create the Alembic configuration, set the database URL, and run the upgrade command. This duplication makes the code harder to maintain and could lead to inconsistencies if the migration logic needs to change.

Consider extracting the migration execution into a shared helper function that both functions can call. This would follow the DRY (Don't Repeat Yourself) principle and ensure consistent migration behavior. Additionally, note that the error handling differs slightly: run_migrations_only exits if alembic.ini doesn't exist (line 39), while start_server only logs a warning (line 73).

Copilot uses AI. Check for mistakes.
Comment on lines 77 to 101
async function main() {
const args = process.argv.slice(2);
const modeArg = args.find((arg) => arg.startsWith('--mode='));
const mode = modeArg ? modeArg.split('=')[1] : 'serve';

switch (mode) {
case 'migrate':
await runMigrationsOnly();
break;
case 'serve':
await startServer(true);
break;
case 'serve-only':
await startServer(false);
break;
default:
console.error(`Invalid mode: ${mode}. Valid modes are: serve, migrate, serve-only`);
process.exit(1);
}
}

main().catch((error) => {
console.error('Fatal error:', error);
process.exit(1);
});
Copy link

Copilot AI Jan 17, 2026

Choose a reason for hiding this comment

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

The new CLI module lacks test coverage. Given that the TypeScript implementation has comprehensive test coverage for other functionality (as evidenced by tests/api.test.ts and tests/integration/), the new operation mode functionality should also have tests to verify:

  • Mode argument parsing (valid and invalid modes)
  • Migration-only mode execution
  • Server-only mode (no migrations run)
  • Default serve mode (migrations + server)
  • Error handling when migrations fail
  • Behavior when USE_POSTGRES is not set

Consider adding a test file like tests/cli.test.ts to ensure the new operation modes work correctly and handle edge cases properly.

Copilot uses AI. Check for mistakes.
Comment on lines 1 to 106
package org.openapitools;

import org.flywaydb.core.Flyway;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.context.ConfigurableApplicationContext;

import javax.sql.DataSource;

/**
* Handles different application startup modes: serve, migrate, and serve-only.
*/
public class ApplicationMode {
private static final Logger logger = LoggerFactory.getLogger(ApplicationMode.class);

public enum Mode {
SERVE, // Default: run migrations then start server
MIGRATE, // Run migrations only and exit
SERVE_ONLY // Start server without running migrations
}

/**
* Run migrations only and exit
*/
public static void runMigrationsOnly(String[] args) {
logger.info("Running migrations only...");

// Start Spring context to get DataSource
System.setProperty("server.port", "0"); // Don't start HTTP server
System.setProperty("spring.flyway.enabled", "true");

try {
ConfigurableApplicationContext context = SpringApplication.run(
OpenApiGeneratorApplication.class, args
);

DataSource dataSource = context.getBean(DataSource.class);

// Run Flyway migrations manually
Flyway flyway = Flyway.configure()
.dataSource(dataSource)
.locations("classpath:db/migration")
.baselineOnMigrate(true)
.validateOnMigrate(true)
.load();

int migrationsExecuted = flyway.migrate().migrationsExecuted;

if (migrationsExecuted > 0) {
logger.info("Successfully executed {} migration(s)", migrationsExecuted);
} else {
logger.info("Database schema is up to date");
}

context.close();
logger.info("Migrations completed successfully");

} catch (Exception e) {
logger.error("Migration failed", e);
System.exit(1);
}
}

/**
* Determine the operation mode from command line arguments
*/
public static Mode parseMode(String[] args) {
for (String arg : args) {
if (arg.startsWith("--mode=")) {
String mode = arg.substring(7).toLowerCase();
switch (mode) {
case "migrate":
return Mode.MIGRATE;
case "serve-only":
return Mode.SERVE_ONLY;
case "serve":
return Mode.SERVE;
default:
logger.error("Invalid mode: {}. Valid modes are: serve, migrate, serve-only", mode);
System.exit(1);
}
}
}
return Mode.SERVE; // Default mode
}

/**
* Configure Spring properties based on the mode
*/
public static void configureMode(Mode mode) {
switch (mode) {
case SERVE:
logger.info("Starting server with automatic migrations...");
System.setProperty("spring.flyway.enabled", "true");
break;
case SERVE_ONLY:
logger.info("Starting server without running migrations...");
System.setProperty("spring.flyway.enabled", "false");
break;
case MIGRATE:
// Handled separately in runMigrationsOnly()
break;
}
}
}
Copy link

Copilot AI Jan 17, 2026

Choose a reason for hiding this comment

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

The new ApplicationMode class lacks test coverage. The Java implementation has comprehensive test coverage for other functionality (as evidenced by src/test/java/org/openapitools/), and the new operation mode functionality should also have tests to verify:

  • Mode parsing from command line arguments (valid and invalid modes)
  • Default mode when no argument is provided
  • Migration-only mode execution and proper context shutdown
  • System property configuration for serve and serve-only modes
  • Error handling and exit codes for failed migrations

Consider adding a test class like ApplicationModeTest.java to ensure the new operation modes work correctly, especially the migration-only mode which starts and stops a Spring context.

Copilot uses AI. Check for mistakes.
else if (mode == "serve-only")
{
Console.WriteLine("Starting server without running migrations...");
}
Copy link

Copilot AI Jan 17, 2026

Choose a reason for hiding this comment

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

The mode parameter is not validated before use. If an invalid mode value (e.g., "invalid-mode") is provided, the code will silently proceed with server startup instead of reporting an error. This could lead to unexpected behavior where an operator mistypes the mode and the application behaves differently than intended.

Add validation to ensure only valid modes (serve, migrate, serve-only) are accepted. Consider adding an else clause after line 95 that logs an error and exits for invalid mode values, consistent with how other implementations (Kotlin, Java, Python, TypeScript) handle invalid modes.

Suggested change
}
}
else
{
Console.WriteLine($"Invalid mode '{mode}'. Valid modes are 'serve', 'migrate', and 'serve-only'.");
Environment.Exit(1);
}

Copilot uses AI. Check for mistakes.
Comment on lines +89 to +126
def main():
"""Main CLI entry point."""
parser = argparse.ArgumentParser(
description="Lamp Control API - FastAPI application for controlling lamps"
)
parser.add_argument(
"--mode",
choices=["serve", "migrate", "serve-only"],
default="serve",
help="Operation mode: serve (default, migrate and start server), "
"migrate (run migrations only), serve-only (start server without migrations)",
)
parser.add_argument(
"--log-level",
choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
default="INFO",
help="Logging level (default: INFO)",
)

args = parser.parse_args()

# Configure logging
logging.basicConfig(
level=getattr(logging, args.log_level),
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
)

# Execute based on mode
if args.mode == "migrate":
run_migrations_only()
elif args.mode == "serve":
start_server(run_migrations=True)
elif args.mode == "serve-only":
start_server(run_migrations=False)


if __name__ == "__main__":
main()
Copy link

Copilot AI Jan 17, 2026

Choose a reason for hiding this comment

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

The new CLI module lacks test coverage. The Python implementation has comprehensive test coverage for other functionality (tests/test_default_api.py, tests/test_health_endpoint.py), and the new operation mode functionality should also have tests to verify:

  • Mode argument parsing for all three modes
  • Migration-only mode execution with and without PostgreSQL configured
  • Server-only mode (ensures migrations are skipped)
  • Default serve mode (migrations + server startup)
  • Error handling when Alembic configuration is missing
  • Error handling when migrations fail

Consider adding a test file like tests/test_cli.py to ensure the new operation modes work correctly and handle edge cases properly. This is especially important given the custom coding guideline that emphasizes running Black and Ruff before commits.

Copilot uses AI. Check for mistakes.
Comment on lines +14 to 66
fun main(args: Array<String>) {
// Parse command line arguments
val mode = args.find { it.startsWith("--mode=") }?.substringAfter("=") ?: "serve"

when (mode) {
"migrate" -> runMigrationsOnly()
"serve" -> startServer(runMigrations = true)
"serve-only" -> startServer(runMigrations = false)
else -> {
logger.error("Invalid mode: $mode. Valid modes are: serve, migrate, serve-only")
exitProcess(1)
}
}
}

/**
* Run database migrations only and exit
*/
fun runMigrationsOnly() {
logger.info("Running migrations only...")
val config = DatabaseConfig.fromEnv()

if (config == null) {
logger.warn("No PostgreSQL configuration found, nothing to migrate")
return
}

logger.info("Running migrations for database: ${config.host}:${config.port}/${config.database}")

val success = FlywayConfig.runMigrations(config)
if (!success) {
logger.error("Migrations failed")
exitProcess(1)
}

logger.info("Migrations completed successfully")
}

/**
* Start the server with optional migrations
*/
fun startServer(runMigrations: Boolean) {
if (runMigrations) {
logger.info("Starting server with automatic migrations...")
} else {
logger.info("Starting server without running migrations...")
// Set system property to skip migrations in DatabaseFactory
System.setProperty("skip.migrations", "true")
}

fun main() {
embeddedServer(Netty, port = 8080, host = "0.0.0.0", module = Application::module)
.start(wait = true)
}
Copy link

Copilot AI Jan 17, 2026

Choose a reason for hiding this comment

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

The new operation mode functionality in Application.kt lacks test coverage. The Kotlin implementation has comprehensive test coverage for other functionality (as shown in src/test/kotlin/com/lampcontrol/), and the new mode handling should also have tests to verify:

  • Argument parsing for all three modes (serve, migrate, serve-only)
  • Invalid mode handling and error messages
  • Migration-only mode execution
  • System property setting for serve-only mode
  • Proper exit codes for failed migrations

Consider adding test cases to verify the new main function behavior and the runMigrationsOnly and startServer functions work correctly with different configurations.

Copilot uses AI. Check for mistakes.
davideme and others added 13 commits January 18, 2026 11:49
Python fixes:
- Add __hash__ method to LampEntity class (required when __eq__ is defined)
- Move HTTPException import to top of default_api_impl.py
- Move datetime and psycopg2 imports to top of test files
- Fix import order in cli.py

TypeScript fixes:
- Add explicit Promise<void> return types to async functions
- Replace console.log with console.warn to comply with ESLint rules
- Add explicit return type annotation to Proxy get function

All linting checks now pass:
- Python: ruff and black
- TypeScript: eslint

Co-authored-by: Claude <noreply@anthropic.com>
Applied consistent code formatting, improved parameter alignment, and enhanced readability across Kotlin source files. No functional changes were made; this is a style and formatting update to improve maintainability and code clarity.
Added 'Bash(golangci-lint run:*)' to the allowed commands in .claude/settings.local.json. Also inserted minor formatting improvements (blank lines) in main.go for better readability.
Replaces CR and LF characters in the mode string with underscores before logging invalid mode errors, mitigating potential CRLF injection vulnerabilities in logs.
Imported LampRepository in LampServiceTest.kt, likely in preparation for new tests or to resolve missing reference errors.
Moved the import of Config from alembic.config below the import of command from alembic for improved readability and consistency.
Added the spotbugs-annotations dependency to the Maven configuration and used @SuppressFBWarnings to suppress the CRLF_INJECTION_LOGS warning in ApplicationMode.java. This ensures static analysis tools do not flag sanitized log statements.
Updated Jacoco test report and coverage verification tasks to exclude Application, AppMain, Configuration, and Paths classes from coverage analysis. This helps focus coverage metrics on relevant code and avoids including boilerplate or configuration classes.
Introduced a short delay before updating lamp status in the integration test to ensure timestamp differences are detectable. This helps prevent flaky tests due to identical timestamps.
Updated Jest configuration to exclude 'src/cli.ts' from code coverage. This helps focus coverage metrics on core source files and not on CLI entry points.
Expanded PMD ruleset exclusions for better alignment with project needs, adding comments for rationale. In ApplicationMode, mode string parsing now uses Locale.ROOT for consistent case conversion.
@davideme davideme merged commit 56c1b75 into main Jan 18, 2026
21 checks passed
@davideme davideme deleted the add-condition-migration branch January 18, 2026 17:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants