Skip to content

Commit 52c3386

Browse files
fix(server): resolve annotated GitHub release tags to commits (#897)
1 parent 1ae361f commit 52c3386

File tree

4 files changed

+195
-11
lines changed

4 files changed

+195
-11
lines changed

server/application-server/src/main/java/de/tum/cit/aet/helios/environment/status/EnvironmentStatusConfig.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,7 @@ public class EnvironmentStatusConfig {
5151
public Duration getCheckInterval() {
5252
if (checkRecentInterval.compareTo(checkStableInterval) > 0) {
5353
throw new IllegalArgumentException(
54-
"Recent interval must be lower or equal to stable interval"
55-
);
54+
"Recent interval must be lower or equal to stable interval");
5655
}
5756
return checkRecentInterval;
5857
}
@@ -114,4 +113,4 @@ public int getMaxPoolSize(int availableProcessors) {
114113
return maxPoolSize != null ? maxPoolSize : availableProcessors * 4;
115114
}
116115
}
117-
}
116+
}

server/application-server/src/main/java/de/tum/cit/aet/helios/github/GitHubMessageHandler.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ protected T parsePayload(byte[] data) throws Exception {
3131
}
3232

3333
/**
34-
* Processes a GitHub webhook event for a repository that is installed
34+
* Processes a GitHub webhook event for a repository that is installed.
3535
*
3636
* @param payload The GitHub event payload for a installed repository. The payload type varies
3737
* depending on the event type (Push, Pull Request, etc.).

server/application-server/src/main/java/de/tum/cit/aet/helios/releaseinfo/release/github/GitHubReleaseSyncService.java

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import org.kohsuke.github.GHRef;
1515
import org.kohsuke.github.GHRelease;
1616
import org.kohsuke.github.GHRepository;
17+
import org.kohsuke.github.GHTagObject;
1718
import org.springframework.stereotype.Service;
1819
import org.springframework.transaction.annotation.Transactional;
1920

@@ -63,19 +64,23 @@ public void processRelease(GHRelease ghRelease, GHRepository ghRepository) {
6364
.orElseGet(
6465
() -> {
6566
try {
66-
GHRepository currentRepository = gitHubService
67-
.getRepository(ghRepository.getFullName());
68-
final GHRef ref = currentRepository.getRef("tags/" + ghRelease.getTagName());
67+
GHRepository currentRepository =
68+
gitHubService.getRepository(ghRepository.getFullName());
69+
final String commitSha =
70+
resolveCommitShaForTag(currentRepository, ghRelease.getTagName());
71+
72+
if (commitSha == null) {
73+
return null;
74+
}
75+
6976
final Commit commit =
7077
commitRepository
71-
.findByShaAndRepositoryRepositoryId(
72-
ref.getObject().getSha(), repository.getRepositoryId())
78+
.findByShaAndRepositoryRepositoryId(commitSha, repository.getRepositoryId())
7379
.orElseGet(
7480
() -> {
7581
try {
7682
return commitSyncService.processCommit(
77-
currentRepository
78-
.getCommit(ref.getObject().getSha()), ghRepository);
83+
currentRepository.getCommit(commitSha), ghRepository);
7984
} catch (IOException e) {
8085
log.error(
8186
"Failed to get commit for release candidate {}: {}",
@@ -85,6 +90,13 @@ public void processRelease(GHRelease ghRelease, GHRepository ghRepository) {
8590
}
8691
});
8792

93+
if (commit == null) {
94+
log.error(
95+
"Skipping release candidate {} because no commit could be resolved",
96+
ghRelease.getTagName());
97+
return null;
98+
}
99+
88100
ReleaseCandidate releaseCandidate = new ReleaseCandidate();
89101
releaseCandidate.setRepository(repository);
90102
releaseCandidate.setName(ghRelease.getTagName());
@@ -102,4 +114,30 @@ public void processRelease(GHRelease ghRelease, GHRepository ghRepository) {
102114
}
103115
});
104116
}
117+
118+
private String resolveCommitShaForTag(GHRepository repository, String tagName)
119+
throws IOException {
120+
GHRef.GHObject object = repository.getRef("tags/" + tagName).getObject();
121+
122+
// Release tags can be lightweight (points directly to commit) or annotated (points to tag
123+
// object). Follow tag objects until we reach the underlying commit.
124+
for (int depth = 0; depth < 5; depth++) {
125+
if ("commit".equals(object.getType())) {
126+
return object.getSha();
127+
}
128+
if (!"tag".equals(object.getType())) {
129+
log.error(
130+
"Unsupported tag object type '{}' for release candidate {}",
131+
object.getType(),
132+
tagName);
133+
return null;
134+
}
135+
GHTagObject tagObject = repository.getTagObject(object.getSha());
136+
object = tagObject.getObject();
137+
}
138+
139+
log.error(
140+
"Failed to resolve commit for release candidate {} due to excessive tag depth", tagName);
141+
return null;
142+
}
105143
}
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
package de.tum.cit.aet.helios.releaseinfo.release.github;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
import static org.junit.jupiter.api.Assertions.assertSame;
5+
import static org.mockito.ArgumentMatchers.any;
6+
import static org.mockito.ArgumentMatchers.anyString;
7+
import static org.mockito.Mockito.mock;
8+
import static org.mockito.Mockito.never;
9+
import static org.mockito.Mockito.verify;
10+
import static org.mockito.Mockito.when;
11+
12+
import de.tum.cit.aet.helios.commit.Commit;
13+
import de.tum.cit.aet.helios.commit.CommitRepository;
14+
import de.tum.cit.aet.helios.commit.github.GitHubCommitSyncService;
15+
import de.tum.cit.aet.helios.github.GitHubService;
16+
import de.tum.cit.aet.helios.gitrepo.GitRepoRepository;
17+
import de.tum.cit.aet.helios.gitrepo.GitRepository;
18+
import de.tum.cit.aet.helios.releaseinfo.release.Release;
19+
import de.tum.cit.aet.helios.releaseinfo.release.ReleaseRepository;
20+
import de.tum.cit.aet.helios.releaseinfo.releasecandidate.ReleaseCandidate;
21+
import de.tum.cit.aet.helios.releaseinfo.releasecandidate.ReleaseCandidateRepository;
22+
import java.io.IOException;
23+
import java.util.Optional;
24+
import org.junit.jupiter.api.BeforeEach;
25+
import org.junit.jupiter.api.Test;
26+
import org.junit.jupiter.api.extension.ExtendWith;
27+
import org.kohsuke.github.GHCommit;
28+
import org.kohsuke.github.GHRef;
29+
import org.kohsuke.github.GHRelease;
30+
import org.kohsuke.github.GHRepository;
31+
import org.kohsuke.github.GHTagObject;
32+
import org.mockito.ArgumentCaptor;
33+
import org.mockito.InjectMocks;
34+
import org.mockito.Mock;
35+
import org.mockito.junit.jupiter.MockitoExtension;
36+
37+
@ExtendWith(MockitoExtension.class)
38+
class GitHubReleaseSyncServiceTest {
39+
40+
@Mock private ReleaseRepository releaseRepository;
41+
@Mock private GitHubReleaseConverter releaseConverter;
42+
@Mock private GitHubCommitSyncService commitSyncService;
43+
@Mock private GitRepoRepository gitRepoRepository;
44+
@Mock private ReleaseCandidateRepository releaseCandidateRepository;
45+
@Mock private CommitRepository commitRepository;
46+
@Mock private GitHubService gitHubService;
47+
48+
@Mock private GHRelease ghRelease;
49+
@Mock private GHRepository ghRepository;
50+
@Mock private GHRepository currentRepository;
51+
@Mock private GHRef ref;
52+
@Mock private GHRef.GHObject refObject;
53+
54+
@InjectMocks private GitHubReleaseSyncService service;
55+
56+
private GitRepository repository;
57+
private Release release;
58+
59+
@BeforeEach
60+
void setUp() throws IOException {
61+
repository = new GitRepository();
62+
repository.setRepositoryId(42L);
63+
repository.setNameWithOwner("owner/repo");
64+
65+
release = new Release();
66+
release.setId(1L);
67+
68+
when(ghRelease.getId()).thenReturn(1L);
69+
when(ghRelease.getTagName()).thenReturn("v1.0.0");
70+
when(ghRepository.getFullName()).thenReturn("owner/repo");
71+
72+
when(gitRepoRepository.findByNameWithOwner("owner/repo")).thenReturn(repository);
73+
when(releaseRepository.findById(1L)).thenReturn(Optional.of(release));
74+
when(releaseRepository.saveAndFlush(release)).thenReturn(release);
75+
when(releaseCandidateRepository.findByRepositoryRepositoryIdAndName(42L, "v1.0.0"))
76+
.thenReturn(Optional.empty());
77+
78+
when(gitHubService.getRepository("owner/repo")).thenReturn(currentRepository);
79+
when(currentRepository.getRef("tags/v1.0.0")).thenReturn(ref);
80+
when(ref.getObject()).thenReturn(refObject);
81+
}
82+
83+
@Test
84+
void processRelease_annotatedTag_resolvesCommitAndSavesReleaseCandidate() throws IOException {
85+
Commit commit = new Commit();
86+
commit.setSha("commit-sha");
87+
88+
when(refObject.getType()).thenReturn("tag");
89+
when(refObject.getSha()).thenReturn("tag-sha");
90+
GHTagObject tagObject = mock(GHTagObject.class);
91+
when(currentRepository.getTagObject("tag-sha")).thenReturn(tagObject);
92+
GHRef.GHObject tagTargetObject = mock(GHRef.GHObject.class);
93+
when(tagObject.getObject()).thenReturn(tagTargetObject);
94+
when(tagTargetObject.getType()).thenReturn("commit");
95+
when(tagTargetObject.getSha()).thenReturn("commit-sha");
96+
when(commitRepository.findByShaAndRepositoryRepositoryId("commit-sha", 42L))
97+
.thenReturn(Optional.of(commit));
98+
when(releaseCandidateRepository.saveAndFlush(any(ReleaseCandidate.class)))
99+
.thenAnswer(invocation -> invocation.getArgument(0));
100+
101+
service.processRelease(ghRelease, ghRepository);
102+
103+
ArgumentCaptor<ReleaseCandidate> captor = ArgumentCaptor.forClass(ReleaseCandidate.class);
104+
verify(releaseCandidateRepository).saveAndFlush(captor.capture());
105+
ReleaseCandidate savedReleaseCandidate = captor.getValue();
106+
assertEquals("v1.0.0", savedReleaseCandidate.getName());
107+
assertSame(repository, savedReleaseCandidate.getRepository());
108+
assertSame(commit, savedReleaseCandidate.getCommit());
109+
verify(currentRepository, never()).getCommit(anyString());
110+
}
111+
112+
@Test
113+
void processRelease_commitLookupFails_doesNotSaveReleaseCandidate() throws IOException {
114+
when(refObject.getType()).thenReturn("commit");
115+
when(refObject.getSha()).thenReturn("missing-commit-sha");
116+
when(commitRepository.findByShaAndRepositoryRepositoryId("missing-commit-sha", 42L))
117+
.thenReturn(Optional.empty());
118+
when(currentRepository.getCommit("missing-commit-sha"))
119+
.thenThrow(new IOException("No commit found"));
120+
121+
service.processRelease(ghRelease, ghRepository);
122+
123+
verify(releaseCandidateRepository, never()).saveAndFlush(any(ReleaseCandidate.class));
124+
}
125+
126+
@Test
127+
void processRelease_lightweightTag_syncsCommitWhenMissingLocally() throws IOException {
128+
Commit commit = new Commit();
129+
commit.setSha("commit-sha");
130+
131+
when(refObject.getType()).thenReturn("commit");
132+
when(refObject.getSha()).thenReturn("commit-sha");
133+
when(commitRepository.findByShaAndRepositoryRepositoryId("commit-sha", 42L))
134+
.thenReturn(Optional.empty());
135+
GHCommit ghCommit = mock(GHCommit.class);
136+
when(currentRepository.getCommit("commit-sha")).thenReturn(ghCommit);
137+
when(commitSyncService.processCommit(ghCommit, ghRepository)).thenReturn(commit);
138+
when(releaseCandidateRepository.saveAndFlush(any(ReleaseCandidate.class)))
139+
.thenAnswer(invocation -> invocation.getArgument(0));
140+
141+
service.processRelease(ghRelease, ghRepository);
142+
143+
verify(commitSyncService).processCommit(ghCommit, ghRepository);
144+
verify(currentRepository, never()).getTagObject(anyString());
145+
verify(releaseCandidateRepository).saveAndFlush(any(ReleaseCandidate.class));
146+
}
147+
}

0 commit comments

Comments
 (0)