Skip to content
Merged
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
9 changes: 5 additions & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,14 @@ RUN pnpm run build
# Stage 2: Production stage
FROM node:20-alpine AS production

# Install system dependencies
# Install system dependencies including tini for zombie process handling
RUN apk add --no-cache \
git \
openssh-client \
curl \
bash \
sudo
sudo \
tini

# Install utilities
RUN apk add --no-cache \
Expand Down Expand Up @@ -109,8 +110,8 @@ USER root
RUN chmod +x /app/entrypoint.sh
USER appuser

# Set entrypoint and default command
ENTRYPOINT ["/app/entrypoint.sh"]
# Set entrypoint and default command with tini as init process
ENTRYPOINT ["/sbin/tini", "--", "/app/entrypoint.sh"]
CMD ["node", "dist/index.js"]

# Labels for metadata
Expand Down
33 changes: 30 additions & 3 deletions scripts/entrypoint.sh
Original file line number Diff line number Diff line change
@@ -1,7 +1,24 @@
#!/bin/bash
set -e

# Signal handling for graceful shutdown
cleanup() {
echo "Received shutdown signal, cleaning up..."
# Send SIGTERM to all child processes
if [ -n "$MAIN_PID" ]; then
echo "Terminating main process (PID: $MAIN_PID)"
kill -TERM "$MAIN_PID" 2>/dev/null || true
wait "$MAIN_PID" 2>/dev/null || true
fi
echo "Cleanup completed"
exit 0
}

# Set up signal handlers
trap cleanup SIGTERM SIGINT

echo "=== AI DevTeam Starting ==="
echo "Container init process: tini (zombie process reaper enabled)"
echo "Node.js version: $(node --version)"
echo "npm version: $(npm --version)"
echo "Git version: $(git --version)"
Expand Down Expand Up @@ -56,7 +73,17 @@ if [ ! -z "$GIT_ACCEPT_HOST_KEY" ] && [ "$GIT_ACCEPT_HOST_KEY" = "true" ]; then
fi

echo "=== Configuration Complete ==="
echo "Starting application..."
echo "Starting application with PID tracking..."

# Execute the main application in background and track PID
"$@" &
MAIN_PID=$!

echo "Main application started (PID: $MAIN_PID)"

# Wait for the main process to complete
wait "$MAIN_PID"
EXIT_CODE=$?

# Execute the main application
exec "$@"
echo "Main application exited with code: $EXIT_CODE"
exit $EXIT_CODE
51 changes: 49 additions & 2 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@
}

// Repository 정보 추출 헬퍼 메서드
private extractRepositoryFromBoardItem(boardItem: any, pullRequestUrl?: string): string {

Check warning on line 73 in src/app.ts

View workflow job for this annotation

GitHub Actions / test

Unexpected any. Specify a different type
return RepositoryInfoExtractor.extractRepositoryFromBoardItem(
boardItem,
pullRequestUrl,
Expand Down Expand Up @@ -370,20 +370,67 @@

// Graceful shutdown을 위한 신호 핸들러 설정
setupSignalHandlers(): void {
let shutdownInProgress = false;

const signalHandler = (signal: string) => {
if (shutdownInProgress) {
console.log(`\n⚠️ ${signal} 신호가 이미 처리 중입니다. 강제 종료하려면 다시 한 번 신호를 보내세요.`);
return;
}

shutdownInProgress = true;
console.log(`\n📡 ${signal} 신호 수신됨. Graceful shutdown 시작...`);

// 강제 종료 타이머 (30초 후)
const forceExitTimeout = setTimeout(() => {
console.error('⚠️ Graceful shutdown이 30초 내에 완료되지 않아 강제 종료합니다.');
process.exit(1);
}, 30000);

this.stop()
.then(() => {
clearTimeout(forceExitTimeout);
console.log('✅ Graceful shutdown 완료');
process.exit(0);
})
.catch((error) => {
clearTimeout(forceExitTimeout);
console.error('❌ Graceful shutdown 실패:', error);
process.exit(1);
});
};

process.on('SIGTERM', () => signalHandler('SIGTERM'));
process.on('SIGINT', () => signalHandler('SIGINT'));
// 두 번째 신호 수신 시 즉시 강제 종료
let signalCount = 0;
const forceSignalHandler = (signal: string) => {
signalCount++;

if (signalCount === 1) {
signalHandler(signal);
} else if (signalCount >= 2) {
console.log(`\n⚡ 두 번째 ${signal} 신호 수신됨. 즉시 강제 종료합니다.`);
process.exit(1);
}
};

process.on('SIGTERM', () => forceSignalHandler('SIGTERM'));
process.on('SIGINT', () => forceSignalHandler('SIGINT'));

// 처리되지 않은 promise rejection 핸들링
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
this.logger?.error('Unhandled promise rejection', { reason, promise });
});

// 처리되지 않은 예외 핸들링
process.on('uncaughtException', (error) => {
console.error('Uncaught Exception:', error);
this.logger?.error('Uncaught exception', { error });

// 정리 후 종료
this.stop()
.finally(() => process.exit(1))
.catch(() => process.exit(1));
});
}
}
Loading
Loading