diff --git a/src/main/java/org/ezcode/codetest/application/submission/dto/event/GitPushStatusEvent.java b/src/main/java/org/ezcode/codetest/application/submission/dto/event/GitPushStatusEvent.java new file mode 100644 index 00000000..42a646e4 --- /dev/null +++ b/src/main/java/org/ezcode/codetest/application/submission/dto/event/GitPushStatusEvent.java @@ -0,0 +1,23 @@ +package org.ezcode.codetest.application.submission.dto.event; + +import org.ezcode.codetest.infrastructure.github.model.PushStatus; + +public record GitPushStatusEvent( + + String sessionKey, + + PushStatus pushStatus + +) { + public static GitPushStatusEvent started(String sessionKey) { + return new GitPushStatusEvent(sessionKey, PushStatus.STARTED); + } + + public static GitPushStatusEvent succeeded(String sessionKey) { + return new GitPushStatusEvent(sessionKey, PushStatus.SUCCESS); + } + + public static GitPushStatusEvent failed(String sessionKey) { + return new GitPushStatusEvent(sessionKey, PushStatus.FAILED); + } +} diff --git a/src/main/java/org/ezcode/codetest/application/submission/dto/request/github/GitHubPushRequest.java b/src/main/java/org/ezcode/codetest/application/submission/dto/request/github/GitHubPushRequest.java index 371952c9..60d7c985 100644 --- a/src/main/java/org/ezcode/codetest/application/submission/dto/request/github/GitHubPushRequest.java +++ b/src/main/java/org/ezcode/codetest/application/submission/dto/request/github/GitHubPushRequest.java @@ -18,10 +18,16 @@ public record GitHubPushRequest( Long problemId, + String difficulty, + String problemTitle, String problemDescription, + String languageName, + + String languageVersion, + Long averageMemoryUsage, Long averageExecutionTime, @@ -31,19 +37,26 @@ public record GitHubPushRequest( String submittedAt ) { - public static GitHubPushRequest of(SubmissionContext ctx, UserGithubInfo info) { + public static GitHubPushRequest of(SubmissionContext ctx, UserGithubInfo info, String decryptedToken) { return new GitHubPushRequest( info.getOwner(), info.getRepo(), info.getBranch(), - info.getGithubAccessToken(), + decryptedToken, ctx.getProblem().getId(), + ctx.getProblem().getDifficulty().getDifficulty(), ctx.getProblem().getTitle(), ctx.getProblem().getDescription(), + ctx.getLanguageName(), + ctx.getLanguageVersion(), ctx.aggregator().averageMemoryUsage(), ctx.aggregator().averageExecutionTime(), ctx.getSourceCode(), LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")) ); } + + public String getLanguage() { + return languageName + " " + languageVersion; + } } diff --git a/src/main/java/org/ezcode/codetest/application/submission/model/SubmissionContext.java b/src/main/java/org/ezcode/codetest/application/submission/model/SubmissionContext.java index 9af46744..2b5d7612 100644 --- a/src/main/java/org/ezcode/codetest/application/submission/model/SubmissionContext.java +++ b/src/main/java/org/ezcode/codetest/application/submission/model/SubmissionContext.java @@ -4,7 +4,6 @@ import org.ezcode.codetest.domain.language.model.entity.Language; import org.ezcode.codetest.domain.problem.model.ProblemInfo; import org.ezcode.codetest.domain.problem.model.entity.Problem; -import org.ezcode.codetest.domain.problem.model.entity.ProblemCategory; import org.ezcode.codetest.domain.problem.model.entity.Testcase; import org.ezcode.codetest.domain.submission.model.SubmissionAggregator; import org.ezcode.codetest.domain.user.model.entity.User; @@ -139,7 +138,7 @@ public void incrementCorrectSubmissions() { getProblem().incrementCorrectSubmissions(); } - public boolean isGitPushStatus(){ + public boolean isGitPushStatus() { return user.isGitPushStatus(); } @@ -150,4 +149,12 @@ public Long getUserId() { public boolean isPassed() { return getPassedCount() == getTestcaseCount(); } + + public String getLanguageName() { + return language.getName(); + } + + public String getLanguageVersion() { + return language.getVersion(); + } } diff --git a/src/main/java/org/ezcode/codetest/application/submission/port/SubmissionEventService.java b/src/main/java/org/ezcode/codetest/application/submission/port/SubmissionEventService.java index 3cc4a98a..14408bb7 100644 --- a/src/main/java/org/ezcode/codetest/application/submission/port/SubmissionEventService.java +++ b/src/main/java/org/ezcode/codetest/application/submission/port/SubmissionEventService.java @@ -1,5 +1,6 @@ package org.ezcode.codetest.application.submission.port; +import org.ezcode.codetest.application.submission.dto.event.GitPushStatusEvent; import org.ezcode.codetest.application.submission.dto.event.SubmissionErrorEvent; import org.ezcode.codetest.application.submission.dto.event.SubmissionJudgingFinishedEvent; import org.ezcode.codetest.application.submission.dto.event.TestcaseListInitializedEvent; @@ -14,4 +15,6 @@ public interface SubmissionEventService { void publishFinalResult(SubmissionJudgingFinishedEvent event); void publishSubmissionError(SubmissionErrorEvent event); + + void publishGitPushStatus(GitPushStatusEvent event); } diff --git a/src/main/java/org/ezcode/codetest/application/submission/service/GitHubPushService.java b/src/main/java/org/ezcode/codetest/application/submission/service/GitHubPushService.java new file mode 100644 index 00000000..eb25bed3 --- /dev/null +++ b/src/main/java/org/ezcode/codetest/application/submission/service/GitHubPushService.java @@ -0,0 +1,43 @@ +package org.ezcode.codetest.application.submission.service; + +import org.ezcode.codetest.application.submission.dto.event.GitPushStatusEvent; +import org.ezcode.codetest.application.submission.dto.request.github.GitHubPushRequest; +import org.ezcode.codetest.application.submission.model.SubmissionContext; +import org.ezcode.codetest.application.submission.port.ExceptionNotifier; +import org.ezcode.codetest.application.submission.port.GitHubClient; +import org.ezcode.codetest.application.submission.port.SubmissionEventService; +import org.ezcode.codetest.common.security.util.AESUtil; +import org.ezcode.codetest.domain.user.model.entity.UserGithubInfo; +import org.ezcode.codetest.domain.user.service.UserGithubService; +import org.springframework.stereotype.Service; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class GitHubPushService { + + private final GitHubClient gitHubClient; + private final UserGithubService userGithubService; + private final ExceptionNotifier exceptionNotifier; + private final SubmissionEventService eventService; + private final AESUtil aesUtil; + + public void pushSolutionToRepo(SubmissionContext ctx) { + if (!ctx.isGitPushStatus() || !ctx.isPassed()) { + return; + } + + eventService.publishGitPushStatus(GitPushStatusEvent.started(ctx.getSessionKey())); + UserGithubInfo info = userGithubService.getUserGithubInfoById(ctx.getUserId()); + + try { + String decryptedToken = aesUtil.decrypt(info.getGithubAccessToken()); + gitHubClient.commitAndPushToRepo(GitHubPushRequest.of(ctx, info, decryptedToken)); + eventService.publishGitPushStatus(GitPushStatusEvent.succeeded(ctx.getSessionKey())); + } catch (Exception e) { + exceptionNotifier.notifyException("commitAndPush", e); + eventService.publishGitPushStatus(GitPushStatusEvent.failed(ctx.getSessionKey())); + } + } +} diff --git a/src/main/java/org/ezcode/codetest/application/submission/service/GitHubService.java b/src/main/java/org/ezcode/codetest/application/submission/service/GitHubService.java deleted file mode 100644 index 46d5decf..00000000 --- a/src/main/java/org/ezcode/codetest/application/submission/service/GitHubService.java +++ /dev/null @@ -1,25 +0,0 @@ -package org.ezcode.codetest.application.submission.service; - -import org.ezcode.codetest.application.submission.dto.request.github.GitHubPushRequest; -import org.ezcode.codetest.application.submission.model.SubmissionContext; -import org.ezcode.codetest.application.submission.port.GitHubClient; -import org.ezcode.codetest.domain.user.model.entity.UserGithubInfo; -import org.ezcode.codetest.domain.user.service.UserGithubService; -import org.springframework.stereotype.Service; - -import lombok.RequiredArgsConstructor; - -@Service -@RequiredArgsConstructor -public class GitHubService { - - private final GitHubClient gitHubClient; - private final UserGithubService userGithubService; - - public void commitAndPushToRepo(SubmissionContext ctx) { - if (ctx.isGitPushStatus() && ctx.isPassed()) { - UserGithubInfo info = userGithubService.getUserGithubInfoById(ctx.getUserId()); - gitHubClient.commitAndPushToRepo(GitHubPushRequest.of(ctx, info)); - } - } -} diff --git a/src/main/java/org/ezcode/codetest/application/submission/service/SubmissionService.java b/src/main/java/org/ezcode/codetest/application/submission/service/SubmissionService.java index ab7c7b05..f4d95f84 100644 --- a/src/main/java/org/ezcode/codetest/application/submission/service/SubmissionService.java +++ b/src/main/java/org/ezcode/codetest/application/submission/service/SubmissionService.java @@ -49,7 +49,7 @@ public class SubmissionService { private final ExceptionNotifier exceptionNotifier; private final LockManager lockManager; private final JudgementService judgementService; - private final GitHubService gitHubService; + private final GitHubPushService gitHubPushService; public String enqueueCodeSubmission(Long problemId, CodeSubmitRequest request, AuthUser authUser) { @@ -68,7 +68,7 @@ public String enqueueCodeSubmission(Long problemId, CodeSubmitRequest request, A @Async("judgeSubmissionExecutor") @Transactional - public void submitCodeStream(SubmissionMessage msg) { + public void processSubmissionAsync(SubmissionMessage msg) { try { log.info("[Submission RUN] Thread = {}", Thread.currentThread().getName()); log.info("[큐 수신] SubmissionMessage.sessionKey: {}", msg.sessionKey()); @@ -77,7 +77,7 @@ public void submitCodeStream(SubmissionMessage msg) { judgementService.publishInitTestcases(ctx); judgementService.runTestcases(ctx); judgementService.finalizeAndPublish(ctx); - gitHubService.commitAndPushToRepo(ctx); + gitHubPushService.pushSolutionToRepo(ctx); } catch (Exception e) { judgementService.publishSubmissionError(msg.sessionKey(), e); exceptionNotifier.notifyException("submitCodeStream", e); diff --git a/src/main/java/org/ezcode/codetest/domain/submission/exception/GitHubClientException.java b/src/main/java/org/ezcode/codetest/domain/submission/exception/GitHubClientException.java new file mode 100644 index 00000000..29000cbe --- /dev/null +++ b/src/main/java/org/ezcode/codetest/domain/submission/exception/GitHubClientException.java @@ -0,0 +1,22 @@ +package org.ezcode.codetest.domain.submission.exception; + +import org.ezcode.codetest.common.base.exception.BaseException; +import org.ezcode.codetest.common.base.exception.ResponseCode; +import org.ezcode.codetest.domain.submission.exception.code.GitHubExceptionCode; +import org.springframework.http.HttpStatus; + +import lombok.Getter; + +@Getter +public class GitHubClientException extends BaseException { + + private final ResponseCode responseCode; + private final HttpStatus httpStatus; + private final String message; + + public GitHubClientException(GitHubExceptionCode responseCode) { + this.responseCode = responseCode; + this.httpStatus = responseCode.getStatus(); + this.message = responseCode.getMessage(); + } +} diff --git a/src/main/java/org/ezcode/codetest/domain/submission/exception/code/GitHubExceptionCode.java b/src/main/java/org/ezcode/codetest/domain/submission/exception/code/GitHubExceptionCode.java new file mode 100644 index 00000000..c535ac42 --- /dev/null +++ b/src/main/java/org/ezcode/codetest/domain/submission/exception/code/GitHubExceptionCode.java @@ -0,0 +1,33 @@ +package org.ezcode.codetest.domain.submission.exception.code; + +import org.ezcode.codetest.common.base.exception.ResponseCode; +import org.springframework.http.HttpStatus; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum GitHubExceptionCode implements ResponseCode { + + INVALID_ACCESS_TOKEN(false, HttpStatus.UNAUTHORIZED, "유효하지 않은 GitHub 액세스 토큰입니다."), + AUTHENTICATION_FAILED(false, HttpStatus.UNAUTHORIZED, "GitHub 인증에 실패했습니다."), + PERMISSION_DENIED(false, HttpStatus.FORBIDDEN, "해당 리소스에 대한 권한이 없습니다."), + + REPOSITORY_NOT_FOUND(false, HttpStatus.NOT_FOUND, "지정한 리포지토리를 찾을 수 없습니다."), + BRANCH_NOT_FOUND(false, HttpStatus.NOT_FOUND, "지정한 브랜치를 찾을 수 없습니다."), + SOURCE_FILE_NOT_FOUND(false, HttpStatus.NOT_FOUND, "소스 파일을 찾을 수 없습니다."), + + TREE_CREATION_FAILED(false, HttpStatus.INTERNAL_SERVER_ERROR, "새 Git 트리 생성에 실패했습니다."), + COMMIT_CREATION_FAILED(false, HttpStatus.INTERNAL_SERVER_ERROR, "커밋 생성에 실패했습니다."), + BRANCH_UPDATE_FAILED(false, HttpStatus.INTERNAL_SERVER_ERROR, "브랜치 포인터 업데이트(푸시)에 실패했습니다."), + + RATE_LIMIT_EXCEEDED(false, HttpStatus.TOO_MANY_REQUESTS, "GitHub API 호출 한도를 초과했습니다."), + NETWORK_ERROR(false, HttpStatus.SERVICE_UNAVAILABLE, "GitHub 서버와의 통신 중 네트워크 오류가 발생했습니다."), + + UNKNOWN_ERROR(false, HttpStatus.INTERNAL_SERVER_ERROR, "알 수 없는 GitHub 연동 오류가 발생했습니다."); + + private final boolean success; + private final HttpStatus status; + private final String message; +} diff --git a/src/main/java/org/ezcode/codetest/infrastructure/event/dto/submission/response/GitPushStatusResponse.java b/src/main/java/org/ezcode/codetest/infrastructure/event/dto/submission/response/GitPushStatusResponse.java new file mode 100644 index 00000000..8167419a --- /dev/null +++ b/src/main/java/org/ezcode/codetest/infrastructure/event/dto/submission/response/GitPushStatusResponse.java @@ -0,0 +1,13 @@ +package org.ezcode.codetest.infrastructure.event.dto.submission.response; + +import org.ezcode.codetest.infrastructure.github.model.PushStatus; + +public record GitPushStatusResponse( + + String displayMessage + +) { + public GitPushStatusResponse(PushStatus pushStatus) { + this(pushStatus.getDisplayMessage()); + } +} diff --git a/src/main/java/org/ezcode/codetest/infrastructure/event/listener/RedisJudgeQueueConsumer.java b/src/main/java/org/ezcode/codetest/infrastructure/event/listener/RedisJudgeQueueConsumer.java index dba370c0..ae98b4f7 100644 --- a/src/main/java/org/ezcode/codetest/infrastructure/event/listener/RedisJudgeQueueConsumer.java +++ b/src/main/java/org/ezcode/codetest/infrastructure/event/listener/RedisJudgeQueueConsumer.java @@ -36,7 +36,7 @@ public void onMessage(MapRecord message) { try { log.info("[컨슈머 수신] {}", msg.sessionKey()); - submissionService.submitCodeStream(msg); + submissionService.processSubmissionAsync(msg); log.info("[컨슈머 ACK] messageId={}", message.getId()); redisTemplate.opsForStream().acknowledge("judge-group", message); diff --git a/src/main/java/org/ezcode/codetest/infrastructure/event/listener/SubmissionEventListener.java b/src/main/java/org/ezcode/codetest/infrastructure/event/listener/SubmissionEventListener.java index 5290ccff..80e02148 100644 --- a/src/main/java/org/ezcode/codetest/infrastructure/event/listener/SubmissionEventListener.java +++ b/src/main/java/org/ezcode/codetest/infrastructure/event/listener/SubmissionEventListener.java @@ -2,10 +2,12 @@ import java.util.List; +import org.ezcode.codetest.application.submission.dto.event.GitPushStatusEvent; import org.ezcode.codetest.application.submission.dto.event.SubmissionErrorEvent; import org.ezcode.codetest.application.submission.dto.event.SubmissionJudgingFinishedEvent; import org.ezcode.codetest.application.submission.dto.event.TestcaseListInitializedEvent; import org.ezcode.codetest.application.submission.dto.event.TestcaseEvaluatedEvent; +import org.ezcode.codetest.infrastructure.event.dto.submission.response.GitPushStatusResponse; import org.ezcode.codetest.infrastructure.event.dto.submission.response.ErrorWsResponse; import org.ezcode.codetest.infrastructure.event.dto.submission.response.SubmissionFinalResultResponse; import org.ezcode.codetest.infrastructure.event.dto.submission.response.InitTestcaseListResponse; @@ -47,4 +49,10 @@ public void onSubmissionError(SubmissionErrorEvent event) { ErrorWsResponse wsDto = ErrorWsResponse.from(event.code()); messageService.sendError(event.sessionKey(), wsDto); } + + @EventListener + public void onGitPushStatus(GitPushStatusEvent event) { + GitPushStatusResponse wsDto = new GitPushStatusResponse(event.pushStatus()); + messageService.sendGitStatus(event.sessionKey(), wsDto); + } } diff --git a/src/main/java/org/ezcode/codetest/infrastructure/event/publisher/StompMessageService.java b/src/main/java/org/ezcode/codetest/infrastructure/event/publisher/StompMessageService.java index 7d813ede..ba45d6ef 100644 --- a/src/main/java/org/ezcode/codetest/infrastructure/event/publisher/StompMessageService.java +++ b/src/main/java/org/ezcode/codetest/infrastructure/event/publisher/StompMessageService.java @@ -1,5 +1,6 @@ package org.ezcode.codetest.infrastructure.event.publisher; +import org.ezcode.codetest.infrastructure.event.dto.submission.response.GitPushStatusResponse; import org.ezcode.codetest.infrastructure.event.dto.submission.response.ErrorWsResponse; import org.ezcode.codetest.infrastructure.event.dto.submission.response.SubmissionFinalResultResponse; import org.ezcode.codetest.infrastructure.event.dto.submission.response.InitTestcaseListResponse; @@ -20,103 +21,113 @@ @RequiredArgsConstructor public class StompMessageService { - private final SimpMessagingTemplate messagingTemplate; + private final SimpMessagingTemplate messagingTemplate; - public void handleChatRoomListLoad(T roomData, String principalName, String sessionId) { + private static final String SUBMISSION_DEST_PREFIX = "/topic/submission/%s"; - SimpMessageHeaderAccessor accessor = - SimpMessageHeaderAccessor.create(SimpMessageType.MESSAGE); + public void handleChatRoomListLoad(T roomData, String principalName, String sessionId) { - accessor.setLeaveMutable(true); - accessor.setSessionId(sessionId); + SimpMessageHeaderAccessor accessor = + SimpMessageHeaderAccessor.create(SimpMessageType.MESSAGE); - messagingTemplate.convertAndSendToUser( - principalName, - "/queue/chatrooms", - roomData, - accessor.getMessageHeaders() - ); - } + accessor.setLeaveMutable(true); + accessor.setSessionId(sessionId); - public void handleChatRoomHistoryLoad(T chatData, String principalName, String sessionId) { + messagingTemplate.convertAndSendToUser( + principalName, + "/queue/chatrooms", + roomData, + accessor.getMessageHeaders() + ); + } - SimpMessageHeaderAccessor accessor = - SimpMessageHeaderAccessor.create(SimpMessageType.MESSAGE); + public void handleChatRoomHistoryLoad(T chatData, String principalName, String sessionId) { - accessor.setLeaveMutable(true); - accessor.setSessionId(sessionId); + SimpMessageHeaderAccessor accessor = + SimpMessageHeaderAccessor.create(SimpMessageType.MESSAGE); - messagingTemplate.convertAndSendToUser( - principalName, - "/queue/chat", - chatData, - accessor.getMessageHeaders() - ); - } + accessor.setLeaveMutable(true); + accessor.setSessionId(sessionId); - public void handleNotification(NotificationResponse data, String principalName) { + messagingTemplate.convertAndSendToUser( + principalName, + "/queue/chat", + chatData, + accessor.getMessageHeaders() + ); + } - messagingTemplate.convertAndSendToUser( - principalName, - "/queue/notification", - data - ); - } + public void handleNotification(NotificationResponse data, String principalName) { - public void handleNotificationList(List dataList, String principalName) { + messagingTemplate.convertAndSendToUser( + principalName, + "/queue/notification", + data + ); + } - messagingTemplate.convertAndSendToUser( - principalName, - "/queue/notifications", - dataList - ); - } + public void handleNotificationList(List dataList, String principalName) { - public void sendInitTestcases(String sessionKey, List dataList) { + messagingTemplate.convertAndSendToUser( + principalName, + "/queue/notifications", + dataList + ); + } - messagingTemplate.convertAndSend( - "/topic/submission/" + sessionKey + "/init", - dataList - ); - } + public void sendInitTestcases(String sessionKey, List dataList) { - public void sendTestcaseResultUpdate(String sessionKey, JudgeResultResponse data) { + messagingTemplate.convertAndSend( + SUBMISSION_DEST_PREFIX.formatted(sessionKey) + "/init", + dataList + ); + } - messagingTemplate.convertAndSend( - "/topic/submission/" + sessionKey + "/case", - data - ); - } + public void sendTestcaseResultUpdate(String sessionKey, JudgeResultResponse data) { - public void sendFinalResult(String sessionKey, SubmissionFinalResultResponse data) { + messagingTemplate.convertAndSend( + SUBMISSION_DEST_PREFIX.formatted(sessionKey) + "/case", + data + ); + } - messagingTemplate.convertAndSend( - "/topic/submission/" + sessionKey + "/final", - data - ); - } + public void sendFinalResult(String sessionKey, SubmissionFinalResultResponse data) { - public void sendError(String sessionKey, ErrorWsResponse data) { + messagingTemplate.convertAndSend( + SUBMISSION_DEST_PREFIX.formatted(sessionKey) + "/final", + data + ); + } - messagingTemplate.convertAndSend( - "/topic/submission/" + sessionKey + "/error", - data - ); - } + public void sendError(String sessionKey, ErrorWsResponse data) { - public void handleChatMessageBroadcast(T data, Long roomId) { + messagingTemplate.convertAndSend( + SUBMISSION_DEST_PREFIX.formatted(sessionKey) + "/error", + data + ); + } - messagingTemplate.convertAndSend("/topic/chat/" + roomId, data); - } + public void sendGitStatus(String sessionKey, GitPushStatusResponse data) { - public void handleChatRoomEntryExitMessage(T messageData, Long roomId) { + messagingTemplate.convertAndSend( + SUBMISSION_DEST_PREFIX.formatted(sessionKey) + "/git-status", + data + ); + } - messagingTemplate.convertAndSend("/topic/chat/" + roomId, messageData); - } + public void handleChatMessageBroadcast(T data, Long roomId) { - public void handleChatRoomParticipantCountChange(T roomData) { + messagingTemplate.convertAndSend("/topic/chat/" + roomId, data); + } - messagingTemplate.convertAndSend("/topic/chatrooms", roomData); - } + public void handleChatRoomEntryExitMessage(T messageData, Long roomId) { + + messagingTemplate.convertAndSend("/topic/chat/" + roomId, messageData); + } + + public void handleChatRoomParticipantCountChange(T roomData) { + + messagingTemplate.convertAndSend("/topic/chatrooms", roomData); + } } diff --git a/src/main/java/org/ezcode/codetest/infrastructure/event/publisher/SubmissionEventPublisher.java b/src/main/java/org/ezcode/codetest/infrastructure/event/publisher/SubmissionEventPublisher.java index d0261358..7c4b19d1 100644 --- a/src/main/java/org/ezcode/codetest/infrastructure/event/publisher/SubmissionEventPublisher.java +++ b/src/main/java/org/ezcode/codetest/infrastructure/event/publisher/SubmissionEventPublisher.java @@ -1,5 +1,6 @@ package org.ezcode.codetest.infrastructure.event.publisher; +import org.ezcode.codetest.application.submission.dto.event.GitPushStatusEvent; import org.ezcode.codetest.application.submission.dto.event.SubmissionErrorEvent; import org.ezcode.codetest.application.submission.dto.event.SubmissionJudgingFinishedEvent; import org.ezcode.codetest.application.submission.dto.event.TestcaseListInitializedEvent; @@ -36,4 +37,9 @@ public void publishSubmissionError(SubmissionErrorEvent event) { publisher.publishEvent(event); } + @Override + public void publishGitPushStatus(GitPushStatusEvent event) { + publisher.publishEvent(event); + } + } diff --git a/src/main/java/org/ezcode/codetest/infrastructure/github/GitBlobCalculator.java b/src/main/java/org/ezcode/codetest/infrastructure/github/GitBlobCalculator.java new file mode 100644 index 00000000..e57e751c --- /dev/null +++ b/src/main/java/org/ezcode/codetest/infrastructure/github/GitBlobCalculator.java @@ -0,0 +1,33 @@ +package org.ezcode.codetest.infrastructure.github; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +import org.springframework.stereotype.Component; + +@Component +public class GitBlobCalculator { + protected String calculateBlobSha(String content) { + try { + byte[] contentBytes = content.getBytes(StandardCharsets.UTF_8); + String header = "blob " + contentBytes.length + "\0"; + byte[] headerBytes = header.getBytes(StandardCharsets.UTF_8); + + byte[] store = new byte[headerBytes.length + contentBytes.length]; + System.arraycopy(headerBytes, 0, store, 0, headerBytes.length); + System.arraycopy(contentBytes, 0, store, headerBytes.length, contentBytes.length); + + MessageDigest md = MessageDigest.getInstance("SHA-1"); + byte[] shaBytes = md.digest(store); + + StringBuilder sb = new StringBuilder(); + for (byte b : shaBytes) { + sb.append(String.format("%02x", b)); + } + return sb.toString(); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("SHA-1 algorithm not available", e); + } + } +} diff --git a/src/main/java/org/ezcode/codetest/infrastructure/github/GitHubApiClient.java b/src/main/java/org/ezcode/codetest/infrastructure/github/GitHubApiClient.java index 5bb8129e..d761a942 100644 --- a/src/main/java/org/ezcode/codetest/infrastructure/github/GitHubApiClient.java +++ b/src/main/java/org/ezcode/codetest/infrastructure/github/GitHubApiClient.java @@ -1,152 +1,207 @@ package org.ezcode.codetest.infrastructure.github; -import java.nio.charset.StandardCharsets; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.Base64; -import java.util.Optional; +import com.fasterxml.jackson.databind.JsonNode; + +import lombok.RequiredArgsConstructor; import org.ezcode.codetest.application.submission.dto.request.github.GitHubPushRequest; -import org.ezcode.codetest.application.submission.port.GitHubClient; -import org.ezcode.codetest.common.security.util.AESUtil; +import org.ezcode.codetest.domain.submission.exception.GitHubClientException; +import org.ezcode.codetest.domain.submission.exception.code.GitHubExceptionCode; +import org.ezcode.codetest.infrastructure.github.model.CommitContext; +import org.ezcode.codetest.infrastructure.github.model.FileType; +import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.HttpStatusCode; import org.springframework.http.MediaType; import org.springframework.stereotype.Component; import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.reactive.function.client.WebClientResponseException; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ObjectNode; - -import lombok.RequiredArgsConstructor; import reactor.core.publisher.Mono; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + @Component @RequiredArgsConstructor -public class GitHubApiClient implements GitHubClient { +public class GitHubApiClient { - private final AESUtil aesUtil; private final WebClient.Builder webClientBuilder; - private final ObjectMapper objectMapper; - - @Override - public void commitAndPushToRepo(GitHubPushRequest request) { - - try { - String owner = request.owner(); - String repo = request.repo(); - String branch = request.branch(); - String accessToken = aesUtil.decrypt(request.accessToken()); - Long problemId = request.problemId(); - String problemTitle = request.problemTitle(); - String problemDescription = request.problemDescription(); - Long averageMemoryUsage = request.averageMemoryUsage(); - Long averageExecutionTime = request.averageExecutionTime(); - String sourceCode = request.sourceCode(); - String submittedAt = request.submittedAt(); - - String mdPath = "test/ezcode/README.md"; - String codePath = String.format("test/ezcode/%s.java", problemTitle); - String commitMsg = String.format("Add solution for %s", problemId); - - WebClient webClient = webClientBuilder - .baseUrl("https://api.github.com") - .defaultHeader(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) - .defaultHeader(HttpHeaders.ACCEPT, "application/vnd.github+json") - .build(); - - String newSha = calculateBlobSha(sourceCode); - Optional existingSha = webClient.get() - .uri(uriBuilder -> uriBuilder - .path("/repos/{owner}/{repo}/contents/{path}") - .queryParam("ref", branch) - .build(owner, repo, codePath) - ) - .retrieve() - .bodyToMono(JsonNode.class) - .map(node -> node.get("sha").asText()) - .onErrorResume(err -> Mono.empty()) - .blockOptional(); - - if (existingSha.filter(sha -> sha.equals(newSha)).isPresent()) { - return; - } - - String md = - """ - # 문제 풀이 상세 - - 사이트: Ezcode - - ## 문제: %s - ### 문제 설명 - %s - - ## 제출 요약 - - 메모리: %sKB - - 실행 시간: %sms - - 제출 일자: %s - """.formatted(problemTitle, problemDescription, averageMemoryUsage, averageExecutionTime, - submittedAt); - String code = String.format("%s", sourceCode); - - ObjectNode mdBody = objectMapper.createObjectNode(); - mdBody.put("message", commitMsg); - mdBody.put("branch", branch); - mdBody.put("content", encodeBase64(md)); - existingSha.ifPresent(sha -> mdBody.put("sha", sha)); - - webClient.put() - .uri("/repos/{owner}/{repo}/contents/{path}", owner, repo, mdPath) - .contentType(MediaType.APPLICATION_JSON) - .bodyValue(mdBody) - .retrieve() - .toBodilessEntity() - .block(); - - ObjectNode codeBody = objectMapper.createObjectNode(); - codeBody.put("message", commitMsg); - codeBody.put("branch", branch); - codeBody.put("content", encodeBase64(code)); - existingSha.ifPresent(sha -> codeBody.put("sha", sha)); - webClient.put() - .uri("/repos/{owner}/{repo}/contents/{path}", owner, repo, codePath) - .contentType(MediaType.APPLICATION_JSON) - .bodyValue(codeBody) - .retrieve() - .toBodilessEntity() - .block(); - - } catch (Exception e) { - throw new RuntimeException(e); - } + @Value("${github.repo.root-folder}") + private String repoRootFolder; + + public static final String REPO_ROOT = "/repos/{owner}/{repo}"; + public static final String CONTENTS_PATH = REPO_ROOT + "/contents/{path}"; + public static final String REF_PATH = REPO_ROOT + "/git/refs/heads/{branch}"; + public static final String COMMITS_PATH = REPO_ROOT + "/git/commits/{sha}"; + public static final String TREES_PATH = REPO_ROOT + "/git/trees"; + public static final String COMMIT_PATH = REPO_ROOT + "/git/commits"; + + protected Optional fetchSourceBlobSha(GitHubPushRequest req) { + String fileName = FileType.SOURCE.resolveFilename(req); + String path = String.format("%s/%s/%s/%s", repoRootFolder, req.difficulty(), req.problemId(), fileName); + + return baseBuilder(req.accessToken()) + .get() + .uri(uriBuilder -> uriBuilder + .path(CONTENTS_PATH) + .queryParam("ref", req.branch()) + .build(req.owner(), req.repo(), path) + ) + .retrieve() + .onStatus(status -> status == HttpStatus.UNAUTHORIZED, + resp -> Mono.error(new GitHubClientException(GitHubExceptionCode.INVALID_ACCESS_TOKEN))) + .onStatus(status -> status == HttpStatus.FORBIDDEN, + resp -> Mono.error(new GitHubClientException(GitHubExceptionCode.PERMISSION_DENIED))) + .onStatus(status -> status == HttpStatus.TOO_MANY_REQUESTS, + resp -> Mono.error(new GitHubClientException(GitHubExceptionCode.RATE_LIMIT_EXCEEDED))) + .onStatus(HttpStatusCode::is5xxServerError, + resp -> Mono.error(new GitHubClientException(GitHubExceptionCode.NETWORK_ERROR))) + .bodyToMono(JsonNode.class) + .map(node -> node.get("sha").asText()) + .onErrorResume(WebClientResponseException.NotFound.class, e -> Mono.empty()) + .onErrorMap(e -> e instanceof GitHubClientException + ? e + : new GitHubClientException(GitHubExceptionCode.UNKNOWN_ERROR)) + .blockOptional(); + } + + protected CommitContext fetchCommitContext(GitHubPushRequest req) { + String headCommitSha = fetchHeadCommitSha(req); + String baseTreeSha = fetchBaseTreeSha(req, headCommitSha); + return new CommitContext(headCommitSha, baseTreeSha); + } + + protected String createTree( + GitHubPushRequest req, String baseTreeSha, List> entries + ) { + Map body = Map.of( + "base_tree", baseTreeSha, + "tree", entries + ); + + JsonNode treeRes = baseBuilder(req.accessToken()) + .post() + .uri(TREES_PATH, req.owner(), req.repo()) + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(body) + .retrieve() + .onStatus(HttpStatusCode::is4xxClientError, + resp -> Mono.error(new GitHubClientException(GitHubExceptionCode.TREE_CREATION_FAILED))) + .onStatus(HttpStatusCode::is5xxServerError, + resp -> Mono.error(new GitHubClientException(GitHubExceptionCode.NETWORK_ERROR))) + .bodyToMono(JsonNode.class) + .block(); + + return Objects.requireNonNull(treeRes).get("sha").asText(); + } + + protected void commitAndPush(GitHubPushRequest req, String parentSha, String treeSha) { + + String commitSha = createCommit(req, parentSha, treeSha); + updateBranchReference(req, commitSha); + + } + + private String fetchHeadCommitSha(GitHubPushRequest req) { + + JsonNode ref = baseBuilder(req.accessToken()) + .get() + .uri(REF_PATH, + req.owner(), req.repo(), req.branch()) + .retrieve() + .onStatus(s -> s == HttpStatus.NOT_FOUND, + resp -> Mono.error(new GitHubClientException(GitHubExceptionCode.BRANCH_NOT_FOUND))) + .onStatus(s -> s == HttpStatus.UNAUTHORIZED, + resp -> Mono.error(new GitHubClientException(GitHubExceptionCode.INVALID_ACCESS_TOKEN))) + .onStatus(HttpStatusCode::is5xxServerError, + resp -> Mono.error(new GitHubClientException(GitHubExceptionCode.NETWORK_ERROR))) + .bodyToMono(JsonNode.class) + .block(); + + return Objects.requireNonNull(ref) + .get("object") + .get("sha") + .asText(); + } + + private String fetchBaseTreeSha(GitHubPushRequest req, String commitSha) { + + JsonNode commit = baseBuilder(req.accessToken()) + .get() + .uri(COMMITS_PATH, + req.owner(), req.repo(), commitSha) + .retrieve() + .onStatus(s -> s == HttpStatus.NOT_FOUND, + resp -> Mono.error(new GitHubClientException(GitHubExceptionCode.REPOSITORY_NOT_FOUND))) + .onStatus(HttpStatusCode::is5xxServerError, + resp -> Mono.error(new GitHubClientException(GitHubExceptionCode.NETWORK_ERROR))) + .bodyToMono(JsonNode.class) + .block(); + + return Objects.requireNonNull(commit) + .get("tree") + .get("sha") + .asText(); + + } + + private String createCommit(GitHubPushRequest req, String parentSha, String treeSha) { + Map body = Map.of( + "message", String.format("문제 %s. %s (%s) – 메모리: %sKB, 시간: %sms", + req.problemId(), + req.problemTitle(), + req.difficulty(), + req.averageMemoryUsage(), + req.averageExecutionTime() + ), + "tree", treeSha, + "parents", List.of(parentSha) + ); + + JsonNode commitRes = baseBuilder(req.accessToken()) + .post() + .uri(COMMIT_PATH, req.owner(), req.repo()) + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(body) + .retrieve() + .onStatus(HttpStatusCode::is4xxClientError, + resp -> Mono.error(new GitHubClientException(GitHubExceptionCode.COMMIT_CREATION_FAILED))) + .onStatus(HttpStatusCode::is5xxServerError, + resp -> Mono.error(new GitHubClientException(GitHubExceptionCode.NETWORK_ERROR))) + .bodyToMono(JsonNode.class) + .block(); + + return Objects.requireNonNull(commitRes).get("sha").asText(); } - public String calculateBlobSha(String content) { - try { - byte[] contentBytes = content.getBytes(StandardCharsets.UTF_8); - String header = "blob " + contentBytes.length + "\0"; - byte[] headerBytes = header.getBytes(StandardCharsets.UTF_8); - - byte[] store = new byte[headerBytes.length + contentBytes.length]; - System.arraycopy(headerBytes, 0, store, 0, headerBytes.length); - System.arraycopy(contentBytes, 0, store, headerBytes.length, contentBytes.length); - - MessageDigest md = MessageDigest.getInstance("SHA-1"); - byte[] shaBytes = md.digest(store); - - StringBuilder sb = new StringBuilder(); - for (byte b : shaBytes) { - sb.append(String.format("%02x", b)); - } - return sb.toString(); - } catch (NoSuchAlgorithmException e) { - throw new RuntimeException("SHA-1 algorithm not available", e); - } + private void updateBranchReference(GitHubPushRequest req, String newCommitSha) { + Map body = Map.of("sha", newCommitSha); + + baseBuilder(req.accessToken()) + .patch() + .uri(REF_PATH, + req.owner(), req.repo(), req.branch()) + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(body) + .retrieve() + .onStatus(HttpStatusCode::is4xxClientError, + resp -> Mono.error(new GitHubClientException(GitHubExceptionCode.BRANCH_UPDATE_FAILED))) + .onStatus(HttpStatusCode::is5xxServerError, + resp -> Mono.error(new GitHubClientException(GitHubExceptionCode.NETWORK_ERROR))) + .toBodilessEntity() + .block(); } - public String encodeBase64(String utf8) { - if (utf8 == null) return ""; - return Base64.getEncoder().encodeToString(utf8.getBytes(StandardCharsets.UTF_8)); + private WebClient baseBuilder(String accessToken) { + return webClientBuilder + .baseUrl("https://api.github.com") + .defaultHeader(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) + .defaultHeader(HttpHeaders.ACCEPT, "application/vnd.github+json") + .build(); } } diff --git a/src/main/java/org/ezcode/codetest/infrastructure/github/GitHubClientImpl.java b/src/main/java/org/ezcode/codetest/infrastructure/github/GitHubClientImpl.java new file mode 100644 index 00000000..21a6e052 --- /dev/null +++ b/src/main/java/org/ezcode/codetest/infrastructure/github/GitHubClientImpl.java @@ -0,0 +1,42 @@ +package org.ezcode.codetest.infrastructure.github; + +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import org.ezcode.codetest.application.submission.dto.request.github.GitHubPushRequest; +import org.ezcode.codetest.application.submission.port.GitHubClient; +import org.ezcode.codetest.infrastructure.github.model.CommitContext; +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class GitHubClientImpl implements GitHubClient { + + private final GitHubApiClient gitHubApiClient; + private final GitHubContentBuilder templateBuilder; + private final GitBlobCalculator blobCalculator; + + @Override + public void commitAndPushToRepo(GitHubPushRequest req) { + String codeBlobSha = blobCalculator.calculateBlobSha(req.sourceCode()); + Optional existingSha = gitHubApiClient.fetchSourceBlobSha(req); + + if (existingSha.map(codeBlobSha::equals).orElse(false)) { + return; + } + + List> entries = templateBuilder.buildGitTreeEntries(req, codeBlobSha); + + CommitContext ctx = gitHubApiClient.fetchCommitContext(req); + String newTreeSha = gitHubApiClient.createTree(req, ctx.baseTreeSha(), entries); + + if (newTreeSha.equals(ctx.baseTreeSha())) { + return; + } + + gitHubApiClient.commitAndPush(req, ctx.headCommitSha(), newTreeSha); + } +} diff --git a/src/main/java/org/ezcode/codetest/infrastructure/github/GitHubContentBuilder.java b/src/main/java/org/ezcode/codetest/infrastructure/github/GitHubContentBuilder.java new file mode 100644 index 00000000..b84f6d79 --- /dev/null +++ b/src/main/java/org/ezcode/codetest/infrastructure/github/GitHubContentBuilder.java @@ -0,0 +1,96 @@ +package org.ezcode.codetest.infrastructure.github; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import lombok.RequiredArgsConstructor; + +import org.ezcode.codetest.application.submission.dto.request.github.GitHubPushRequest; +import org.ezcode.codetest.infrastructure.github.model.FileType; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class GitHubContentBuilder { + + @Value("${github.repo.root-folder}") + private String repoRootFolder; + + private final GitBlobCalculator gitBlobCalculator; + + protected List> buildGitTreeEntries( + GitHubPushRequest req, String codeBlobSha + ) { + String mdContent = buildMarkdown(req); + String mdBlobSha = gitBlobCalculator.calculateBlobSha(mdContent); + + Map blobMap = Map.of( + FileType.README.name(), mdBlobSha, + FileType.SOURCE.name(), codeBlobSha + ); + + return buildGitTreeEntriesFromMap(req, blobMap); + } + + private String buildMarkdown(GitHubPushRequest req) { + return """ + # %s. %s + - 제출 언어: %s + - 제출 일자: %s + + %s + + + ### 제출 요약 + - 메모리: %sKB + - 실행 시간: %sms + + > EzCode + """.formatted( + req.problemId(), + req.problemTitle(), + req.getLanguage(), + formatSubmittedAt(req.submittedAt()), + req.problemDescription(), + req.averageMemoryUsage(), + req.averageExecutionTime() + ); + } + + private List> buildGitTreeEntriesFromMap( + GitHubPushRequest req, + Map blobShaMap + ) { + return blobShaMap.keySet().stream() + .map(s -> { + FileType fileType = FileType.valueOf(s); + String path = String.format("%s/%s/%s/%s", + repoRootFolder, req.difficulty(), req.problemId(), fileType.resolveFilename(req) + ); + + String content = fileType == FileType.SOURCE + ? req.sourceCode() + : buildMarkdown(req); + + return Map.of( + "path", path, + "mode", "100644", + "type", "blob", + "content", content, + "encoding", "utf-8" + ); + }) + .collect(Collectors.toList()); + } + + private String formatSubmittedAt(String timestamp) { + DateTimeFormatter inputFmt = DateTimeFormatter.ofPattern("yyyyMMddHHmmss"); + DateTimeFormatter outputFmt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + LocalDateTime dt = LocalDateTime.parse(timestamp, inputFmt); + return dt.format(outputFmt); + } +} diff --git a/src/main/java/org/ezcode/codetest/infrastructure/github/model/CommitContext.java b/src/main/java/org/ezcode/codetest/infrastructure/github/model/CommitContext.java new file mode 100644 index 00000000..e1ae2244 --- /dev/null +++ b/src/main/java/org/ezcode/codetest/infrastructure/github/model/CommitContext.java @@ -0,0 +1,10 @@ +package org.ezcode.codetest.infrastructure.github.model; + +public record CommitContext( + + String headCommitSha, + + String baseTreeSha + +) { +} diff --git a/src/main/java/org/ezcode/codetest/infrastructure/github/model/Extensions.java b/src/main/java/org/ezcode/codetest/infrastructure/github/model/Extensions.java new file mode 100644 index 00000000..8d1f1e00 --- /dev/null +++ b/src/main/java/org/ezcode/codetest/infrastructure/github/model/Extensions.java @@ -0,0 +1,24 @@ +package org.ezcode.codetest.infrastructure.github.model; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum Extensions { + + JAVA("java"), + C("c"), + CPP("cpp"), + PYTHON("py"); + + private final String extension; + + public static String getExtensionByLanguage(String languageName) { + try { + return Extensions.valueOf(languageName.toUpperCase()).getExtension(); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("지원하지 않는 언어입니다: " + languageName); + } + } +} diff --git a/src/main/java/org/ezcode/codetest/infrastructure/github/model/FileType.java b/src/main/java/org/ezcode/codetest/infrastructure/github/model/FileType.java new file mode 100644 index 00000000..c03840cb --- /dev/null +++ b/src/main/java/org/ezcode/codetest/infrastructure/github/model/FileType.java @@ -0,0 +1,18 @@ +package org.ezcode.codetest.infrastructure.github.model; + +import org.ezcode.codetest.application.submission.dto.request.github.GitHubPushRequest; + +public enum FileType { + SOURCE, README; + + public String resolveFilename(GitHubPushRequest req) { + return switch (this) { + case SOURCE -> String.format("%s.%s", + req.problemTitle(), Extensions.getExtensionByLanguage(req.languageName()) + ); + case README -> String.format("README_%s.md", + req.languageName().toLowerCase() + ); + }; + } +} diff --git a/src/main/java/org/ezcode/codetest/infrastructure/github/model/PushStatus.java b/src/main/java/org/ezcode/codetest/infrastructure/github/model/PushStatus.java new file mode 100644 index 00000000..8764143a --- /dev/null +++ b/src/main/java/org/ezcode/codetest/infrastructure/github/model/PushStatus.java @@ -0,0 +1,17 @@ +package org.ezcode.codetest.infrastructure.github.model; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum PushStatus { + + STARTED("푸시 시작"), + + SUCCESS("푸시 성공"), + + FAILED("푸시 실패 - 잠시 후 다시 시도해주세요."); + + private final String displayMessage; +} diff --git a/src/main/java/org/ezcode/codetest/presentation/submission/SubmissionController.java b/src/main/java/org/ezcode/codetest/presentation/submission/SubmissionController.java index 2d2751ee..af154e60 100644 --- a/src/main/java/org/ezcode/codetest/presentation/submission/SubmissionController.java +++ b/src/main/java/org/ezcode/codetest/presentation/submission/SubmissionController.java @@ -45,11 +45,17 @@ public class SubmissionController { 문제에 대한 코드를 제출하면 채점 큐에 등록되고, 서버는 WebSocket(STOMP)을 통해 채점 결과를 실시간으로 전송합니다. - 반환된 sessionKey를 사용해 다음 경로로 구독하십시오: + 반환된 sessionKey를 사용해 다음 경로로 구독하세요. + • /topic/submission/{sessionKey}/init + • /topic/submission/{sessionKey}/case + • /topic/submission/{sessionKey}/final + • /topic/submission/{sessionKey}/error + + • /topic/submission/{sessionKey}/git-status """ ) @ApiResponses({ diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 04adfe72..949a582c 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -131,7 +131,6 @@ spring.security.oauth2.client.registration.github.scope=user,repo #logging.level.org.springframework.security.oauth2.client=TRACE #logging.level.org.springframework.web.client.RestTemplate=TRACE - # ======================== # S3 # ======================== @@ -145,4 +144,9 @@ cloud.aws.stack.auto=false # ======================== # AES # ======================== -aes.secret.key=${AES_SECRET_KEY} \ No newline at end of file +aes.secret.key=${AES_SECRET_KEY} + +# ======================== +# git push +# ======================== +github.repo.root-folder=ezcode