Skip to content

Commit 7dbe79b

Browse files
author
Mario Ferreira
committed
Keep the problems from a partly-applied sync
Found by actually syncing a real repository rather than a stub: the outcome said "3 created, 1 problem(s)" and the problem itself was nowhere to be seen. Problems lived only in the response to a manual sync. A page reload lost them, and a webhook-triggered sync never showed them at all — so on the trigger that matters most, the operator learned that something was skipped but never what. Reporting a count without the detail is worse than not reporting it, because it looks like information. Now stored on the configuration and read back from there, so the manual and webhook paths cannot disagree. Cleared by the next clean run.
1 parent d32e959 commit 7dbe79b

6 files changed

Lines changed: 88 additions & 9 deletions

File tree

‎backend/src/main/java/com/devforge/sync/application/SyncRunner.java‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,8 @@ private SyncOutcome record(SyncConfiguration configuration, SyncOutcome outcome,
245245
outcome.created(),
246246
outcome.updated(),
247247
outcome.archived(),
248-
outcome.unchanged());
248+
outcome.unchanged(),
249+
outcome.problems());
249250
}
250251

251252
auditTrail.record(actorId, AuditEntry

‎backend/src/main/java/com/devforge/sync/application/SyncService.java‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ public SyncSettingsResponse describe(UUID workspaceId, UUID userId) {
5757
workspaceAccess.requireAccess(workspaceId, userId, WorkspaceRole.ADMIN);
5858

5959
return repository.findByWorkspaceId(workspaceId)
60-
.map(configuration -> SyncSettingsResponse.from(configuration, List.of()))
60+
.map(configuration -> SyncSettingsResponse.from(configuration))
6161
.orElseGet(SyncSettingsResponse::notConfigured);
6262
}
6363

@@ -105,7 +105,7 @@ public SyncSettingsResponse configure(
105105
.with("enabled", saved.isEnabled())
106106
.with("firstTime", isNew));
107107

108-
return SyncSettingsResponse.from(saved, List.of());
108+
return SyncSettingsResponse.from(saved);
109109
}
110110

111111
/**
@@ -147,7 +147,7 @@ public SyncSettingsResponse rotateWebhookUrl(UUID workspaceId, UUID userId) {
147147
.inWorkspace(workspaceId)
148148
.with("webhookUrlRotated", true));
149149

150-
return SyncSettingsResponse.from(configuration, List.of());
150+
return SyncSettingsResponse.from(configuration);
151151
}
152152

153153
@Transactional
@@ -171,8 +171,10 @@ public SyncSettingsResponse syncNow(UUID workspaceId, UUID userId) {
171171
workspaceAccess.requireAccess(workspaceId, userId, WorkspaceRole.ADMIN);
172172
SyncConfiguration configuration = require(workspaceId);
173173

174-
SyncOutcome outcome = runner.run(configuration, userId);
175-
return SyncSettingsResponse.from(configuration, outcome.problems());
174+
runner.run(configuration, userId);
175+
// Read back from the configuration rather than from the outcome: the two
176+
// must agree, and a webhook-triggered sync only ever populates the former.
177+
return SyncSettingsResponse.from(configuration);
176178
}
177179

178180
/**

‎backend/src/main/java/com/devforge/sync/application/SyncSettingsResponse.java‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ public record SyncSettingsResponse(
4646
List<String> problems
4747
) {
4848

49-
public static SyncSettingsResponse from(SyncConfiguration configuration, List<String> problems) {
49+
public static SyncSettingsResponse from(SyncConfiguration configuration) {
5050
return new SyncSettingsResponse(
5151
configuration.isConfigured(),
5252
configuration.getRepositoryUrl(),
@@ -68,7 +68,7 @@ public static SyncSettingsResponse from(SyncConfiguration configuration, List<St
6868
configuration.getLastUpdated(),
6969
configuration.getLastArchived(),
7070
configuration.getLastUnchanged(),
71-
problems == null ? List.of() : problems);
71+
configuration.getLastProblems());
7272
}
7373

7474
/** Before anything has been set up. */

‎backend/src/main/java/com/devforge/sync/domain/SyncConfiguration.java‎

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import jakarta.persistence.Table;
1010

1111
import java.time.Instant;
12+
import java.util.List;
1213
import java.time.temporal.ChronoUnit;
1314
import java.util.UUID;
1415

@@ -86,6 +87,15 @@ public class SyncConfiguration extends BaseEntity {
8687
@Column(name = "last_unchanged", nullable = false)
8788
private int lastUnchanged;
8889

90+
/**
91+
* What the last attempt could not use, newline separated.
92+
*
93+
* <p>Persisted rather than returned once, so a reload still shows them and a
94+
* webhook-triggered sync shows them at all.
95+
*/
96+
@Column(name = "last_problems", columnDefinition = "text")
97+
private String lastProblems;
98+
8999
protected SyncConfiguration() {
90100
// for JPA
91101
}
@@ -158,8 +168,12 @@ public void recordSuccess(
158168
int created,
159169
int updated,
160170
int archived,
161-
int unchanged
171+
int unchanged,
172+
List<String> problems
162173
) {
174+
this.lastProblems = problems == null || problems.isEmpty()
175+
? null
176+
: String.join("\n", problems);
163177
this.lastStatus = status;
164178
this.lastRef = ref;
165179
this.lastMessage = message;
@@ -179,6 +193,7 @@ public void recordSuccess(
179193
public void recordFailure(String message) {
180194
this.lastStatus = SyncStatus.FAILED;
181195
this.lastMessage = message;
196+
this.lastProblems = null;
182197
this.lastCreated = 0;
183198
this.lastUpdated = 0;
184199
this.lastArchived = 0;
@@ -268,4 +283,11 @@ public int getLastArchived() {
268283
public int getLastUnchanged() {
269284
return lastUnchanged;
270285
}
286+
287+
/** Empty when the last attempt used everything it found. */
288+
public List<String> getLastProblems() {
289+
return lastProblems == null || lastProblems.isBlank()
290+
? List.of()
291+
: List.of(lastProblems.split("\n"));
292+
}
271293
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
-- Keep the detail of a partly-applied sync, not just its count.
2+
--
3+
-- The outcome already recorded "3 created, 1 problem(s)", but the problems
4+
-- themselves lived only in the response to a manual sync. So a reload lost them,
5+
-- and a webhook-triggered sync never showed them at all — which makes reporting a
6+
-- problem pointless, since the operator cannot act on a number.
7+
--
8+
-- Newline separated rather than a table: they belong to the last attempt only, and
9+
-- are replaced wholesale by the next one.
10+
ALTER TABLE sync_configurations
11+
ADD COLUMN last_problems TEXT;

‎backend/src/test/java/com/devforge/sync/api/GitSyncIntegrationTest.java‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,49 @@ void appliesWhatItUnderstandsAndReportsWhatItDoesNot() throws Exception {
357357
org.hamcrest.Matchers.containsString("NONSENSE")));
358358
}
359359

360+
/**
361+
* The regression this guards: problems used to live only in the response to a
362+
* manual sync, so a reload lost them and a webhook-triggered sync never showed
363+
* them at all. Reporting "1 problem(s)" without saying which is useless.
364+
*/
365+
@Test
366+
void keepsTheProblemsAfterTheResponseThatReportedThem() throws Exception {
367+
TestUser owner = registerUser("owner@acme.test", "Owner");
368+
UUID ws = createWorkspace(owner, "Platform", "platform");
369+
configure(owner, ws, "ARCHIVE");
370+
371+
source.files = List.of(
372+
new SourceFile("docs/good.md", "# Good"),
373+
new SourceFile("docs/odd.md", "---\ntype: NONSENSE\n---\nbody"));
374+
375+
mockMvc.perform(authed(post("/api/workspaces/{w}/sync/run", ws), owner))
376+
.andExpect(jsonPath("$.problems[0]").value(
377+
org.hamcrest.Matchers.containsString("NONSENSE")));
378+
379+
// Read fresh, as a reload would.
380+
mockMvc.perform(authed(get("/api/workspaces/{w}/sync", ws), owner))
381+
.andExpect(jsonPath("$.lastStatus").value("PARTIAL"))
382+
.andExpect(jsonPath("$.problems[0]").value(
383+
org.hamcrest.Matchers.containsString("NONSENSE")));
384+
}
385+
386+
/** A clean run must not leave the previous run's problems lying around. */
387+
@Test
388+
void clearsTheProblemsWhenTheNextSyncIsClean() throws Exception {
389+
TestUser owner = registerUser("owner@acme.test", "Owner");
390+
UUID ws = createWorkspace(owner, "Platform", "platform");
391+
configure(owner, ws, "ARCHIVE");
392+
393+
source.files = List.of(new SourceFile("docs/odd.md", "---\ntype: NONSENSE\n---\nbody"));
394+
mockMvc.perform(authed(post("/api/workspaces/{w}/sync/run", ws), owner))
395+
.andExpect(jsonPath("$.problems.length()").value(1));
396+
397+
source.files = List.of(new SourceFile("docs/odd.md", "---\ntype: RUNBOOK\n---\nbody"));
398+
mockMvc.perform(authed(post("/api/workspaces/{w}/sync/run", ws), owner))
399+
.andExpect(jsonPath("$.lastStatus").value("OK"))
400+
.andExpect(jsonPath("$.problems.length()").value(0));
401+
}
402+
360403
@Test
361404
void recordsEverySyncInTheWorkspaceActivityLog() throws Exception {
362405
TestUser owner = registerUser("owner@acme.test", "Owner");

0 commit comments

Comments
 (0)