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
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import org.springframework.web.client.RestClientResponseException;

import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Base64;
import java.util.List;
Expand Down Expand Up @@ -147,9 +147,7 @@ private FileContentResponse getFileContent(String userToken, String owner, Strin
public WorkflowRunStatus getLatestRunStatus(String userToken, String repoFullName,
String workflowFileName, LocalDateTime afterTime) {
String[] parts = splitRepo(repoFullName);
String createdFilter = afterTime.minusMinutes(1)
.atOffset(ZoneOffset.UTC)
.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME);
String createdFilter = createdFilter(afterTime, ZoneId.systemDefault());
try {
WorkflowRunsResponse response = restClient(userToken)
.get()
Expand Down Expand Up @@ -254,9 +252,7 @@ private WorkflowRunsResponse getWorkflowRuns(String userToken,
String workflowFileName,
LocalDateTime afterTime) {
String[] parts = splitRepo(repoFullName);
String createdFilter = afterTime.minusMinutes(1)
.atOffset(ZoneOffset.UTC)
.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME);
String createdFilter = createdFilter(afterTime, ZoneId.systemDefault());
try {
return restClient(userToken)
.get()
Expand All @@ -270,6 +266,29 @@ private WorkflowRunsResponse getWorkflowRuns(String userToken,
}
}

/**
* GitHub Actions run 목록의 {@code created=>=} 필터 값을 만든다.
*
* afterTime 은 우리 DB 의 LocalDateTime 이고, 그 값은 호스트 타임존(KST)의 벽시계다.
* 여기서 atOffset(UTC) 를 쓰면 그 벽시계에 UTC 라벨만 붙어 실제보다 9시간 미래의 순간이
* 된다. GitHub 은 진짜 UTC 로 필터링하므로 우리가 찾으려는 실행은 항상 범위 밖으로
* 밀려나고, 결과는 언제나 0건이 된다.
*
* 실측(2026-08-19 운영): triggeredAt 2026-08-18T14:37:40(KST) 인 배포를
* created>=2026-08-18T14:36:40Z 로 조회 → 0건
* created>=2026-08-18T05:36:40Z 로 조회 → 3건, 첫 건이 completed success
* 이 때문에 디스패치 직후 매칭이 늘 실패해 runId 없이 IN_PROGRESS 로 넘어갔고,
* 웹훅을 놓친 이력을 회수하려던 워커도 실행을 못 찾아 성공한 배포를 FAILED 로 닫았다.
*
* 그러니 라벨을 붙이지 말고 벽시계를 그 타임존의 순간으로 해석해야 한다.
*/
static String createdFilter(LocalDateTime afterTime, ZoneId zone) {
return afterTime.minusMinutes(1)
.atZone(zone)
.toOffsetDateTime()
.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME);
}

private void sleep(long retryIntervalMs) {
try {
Thread.sleep(retryIntervalMs);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package com.example.dvely.deployment.infrastructure.external;

import static org.assertj.core.api.Assertions.assertThat;

import java.time.LocalDateTime;
import java.time.ZoneId;
import org.junit.jupiter.api.Test;

class GithubActionsClientTest {

private static final ZoneId SEOUL = ZoneId.of("Asia/Seoul");

@Test
void createdFilterConvertsWallClockToTheRealInstant() {
// DB 의 LocalDateTime 은 호스트 타임존(KST)의 벽시계다. GitHub 은 진짜 UTC 로
// 필터링하므로 그 벽시계를 KST 의 순간으로 해석해 넘겨야 한다.
LocalDateTime triggeredAt = LocalDateTime.of(2026, 8, 18, 14, 37, 40);

String filter = GithubActionsClient.createdFilter(triggeredAt, SEOUL);

// 14:36:40 KST == 05:36:40 UTC. 라벨만 바꾸면 14:36:40Z 가 되어 9시간 미래를 본다.
assertThat(filter).isEqualTo("2026-08-18T14:36:40+09:00");
assertThat(java.time.OffsetDateTime.parse(filter).toInstant())
.isEqualTo(java.time.Instant.parse("2026-08-18T05:36:40Z"));
}

@Test
void theFilterNeverExcludesTheRunItIsLookingFor() {
// 실측(2026-08-19 운영): 이 배포의 실행은 05:38:03Z 에 있었는데, 잘못된 변환은
// 14:36:40Z 부터를 요구해 실행을 범위 밖으로 밀어냈다 — 조회 결과가 늘 0건이었다.
LocalDateTime triggeredAt = LocalDateTime.of(2026, 8, 18, 14, 37, 40);
java.time.Instant actualRunCreatedAt = java.time.Instant.parse("2026-08-18T05:38:03Z");

java.time.Instant filterFrom =
java.time.OffsetDateTime.parse(GithubActionsClient.createdFilter(triggeredAt, SEOUL)).toInstant();

assertThat(filterFrom).isBefore(actualRunCreatedAt);
}

@Test
void aOneMinuteMarginIsKeptSoARunStartedJustBeforeTheRecordStillMatches() {
// 이력의 triggeredAt 과 GitHub 이 실행을 만든 시각은 몇 초 어긋날 수 있다.
LocalDateTime triggeredAt = LocalDateTime.of(2026, 8, 18, 14, 37, 40);

String filter = GithubActionsClient.createdFilter(triggeredAt, SEOUL);

assertThat(java.time.OffsetDateTime.parse(filter).toLocalDateTime())
.isEqualTo(triggeredAt.minusMinutes(1));
}

@Test
void aUtcHostProducesTheSameInstantForItsOwnWallClock() {
// 호스트가 UTC 로 떠 있으면 그 DB 값도 UTC 벽시계다. 시스템 타임존을 그대로 쓰므로
// 어느 쪽이든 순간이 어긋나지 않는다.
LocalDateTime triggeredAt = LocalDateTime.of(2026, 8, 18, 5, 37, 40);

String filter = GithubActionsClient.createdFilter(triggeredAt, ZoneId.of("UTC"));

assertThat(java.time.OffsetDateTime.parse(filter).toInstant())
.isEqualTo(java.time.Instant.parse("2026-08-18T05:36:40Z"));
}
}
Loading