Skip to content

Commit 2cfd4a7

Browse files
authored
Merge pull request #302 from iflytek/fix/rerelease-precheck-warnings
fix(rerelease): support precheck warning confirmation flow
2 parents 5d87a0c + 7c2f06d commit 2cfd4a7

11 files changed

Lines changed: 257 additions & 19 deletions

File tree

server/skillhub-app/src/main/java/com/iflytek/skillhub/dto/SkillVersionRereleaseRequest.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
public record SkillVersionRereleaseRequest(
66
@NotBlank(message = "{validation.required}")
7-
String targetVersion
7+
String targetVersion,
8+
boolean confirmWarnings
89
) {
910
}

server/skillhub-app/src/main/java/com/iflytek/skillhub/service/SkillLifecycleAppService.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,8 @@ public SkillLifecycleMutationResponse rereleaseVersion(String namespace,
154154
skillVersion.getVersion(),
155155
targetVersion,
156156
userId,
157-
normalizeRoles(userNamespaceRoles)
157+
normalizeRoles(userNamespaceRoles),
158+
request.confirmWarnings()
158159
);
159160
auditLogService.record(
160161
userId,

server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/portal/SkillLifecycleControllerTest.java

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,8 @@ void rereleaseVersion_returnsUnifiedEnvelope() throws Exception {
212212
eq("1.2.3"),
213213
eq("1.2.4"),
214214
eq("usr_1"),
215-
anyMap()))
215+
anyMap(),
216+
eq(false)))
216217
.willReturn(new SkillPublishService.PublishResult(1L, "demo-skill", newVersion));
217218

218219
mockMvc.perform(post("/api/web/skills/global/demo-skill/versions/1.2.3/rerelease")
@@ -279,7 +280,8 @@ void rereleaseVersion_trimsTargetVersionBeforeDelegating() throws Exception {
279280
eq("1.2.3"),
280281
eq("1.2.4"),
281282
eq("usr_1"),
282-
anyMap()))
283+
anyMap(),
284+
eq(false)))
283285
.willReturn(new SkillPublishService.PublishResult(1L, "demo-skill", newVersion));
284286

285287
mockMvc.perform(post("/api/web/skills/global/demo-skill/versions/1.2.3/rerelease")
@@ -299,7 +301,44 @@ void rereleaseVersion_trimsTargetVersionBeforeDelegating() throws Exception {
299301
eq("1.2.3"),
300302
eq("1.2.4"),
301303
eq("usr_1"),
302-
anyMap());
304+
anyMap(),
305+
eq(false));
306+
}
307+
308+
@Test
309+
void rereleaseVersion_passesConfirmWarningsToService() throws Exception {
310+
Namespace namespace = new Namespace("global", "Global", "owner");
311+
setNamespaceId(namespace, 1L);
312+
Skill skill = new Skill(1L, "demo-skill", "owner", SkillVisibility.PUBLIC);
313+
setSkillId(skill, 1L);
314+
SkillVersion newVersion = new SkillVersion(1L, "1.2.4", "owner");
315+
setSkillVersionId(newVersion, 3L);
316+
newVersion.setStatus(SkillVersionStatus.PUBLISHED);
317+
318+
given(namespaceRepository.findBySlug("global")).willReturn(java.util.Optional.of(namespace));
319+
given(skillSlugResolutionService.resolve(1L, "demo-skill", "usr_1", SkillSlugResolutionService.Preference.CURRENT_USER))
320+
.willReturn(skill);
321+
SkillVersion sourceVersion = new SkillVersion(1L, "1.2.3", "owner");
322+
setSkillVersionId(sourceVersion, 2L);
323+
sourceVersion.setStatus(SkillVersionStatus.PUBLISHED);
324+
given(skillVersionRepository.findBySkillIdAndVersion(1L, "1.2.3")).willReturn(java.util.Optional.of(sourceVersion));
325+
given(skillPublishService.rereleasePublishedVersion(
326+
eq(1L), eq("1.2.3"), eq("1.2.4"), eq("usr_1"), anyMap(), eq(true)))
327+
.willReturn(new SkillPublishService.PublishResult(1L, "demo-skill", newVersion));
328+
329+
mockMvc.perform(post("/api/web/skills/global/demo-skill/versions/1.2.3/rerelease")
330+
.requestAttr("userId", "usr_1")
331+
.requestAttr("userNsRoles", java.util.Map.of(1L, NamespaceRole.ADMIN))
332+
.contentType(MediaType.APPLICATION_JSON)
333+
.content("{\"targetVersion\":\"1.2.4\",\"confirmWarnings\":true}")
334+
.with(user("usr_1"))
335+
.with(csrf()))
336+
.andExpect(status().isOk())
337+
.andExpect(jsonPath("$.code").value(0))
338+
.andExpect(jsonPath("$.data.action").value("RERELEASE_VERSION"));
339+
340+
verify(skillPublishService).rereleasePublishedVersion(
341+
eq(1L), eq("1.2.3"), eq("1.2.4"), eq("usr_1"), anyMap(), eq(true));
303342
}
304343

305344
private Skill skillWithStatus(Skill skill, com.iflytek.skillhub.domain.skill.SkillStatus status) {

server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillPublishService.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,8 @@ public PublishResult rereleasePublishedVersion(
156156
String sourceVersion,
157157
String targetVersion,
158158
String publisherId,
159-
Map<Long, NamespaceRole> userNamespaceRoles) {
159+
Map<Long, NamespaceRole> userNamespaceRoles,
160+
boolean confirmWarnings) {
160161
Skill skill = skillRepository.findById(skillId)
161162
.orElseThrow(() -> new DomainBadRequestException("error.skill.notFound", skillId));
162163
assertCanManageLifecycle(skill, publisherId, userNamespaceRoles);
@@ -181,7 +182,7 @@ public PublishResult rereleasePublishedVersion(
181182
publisherId,
182183
skill.getVisibility(),
183184
Set.of(),
184-
false, // confirmWarnings=false: no warnings to confirm for rerelease
185+
confirmWarnings, // confirmWarnings: honour caller's choice for rerelease
185186
false, // forceAutoPublish=false: respect visibility rules
186187
true
187188
);

server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/service/SkillPublishServiceTest.java

Lines changed: 133 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -855,7 +855,8 @@ void testRereleasePublishedVersion_ShouldCloneFilesAndSubmitForReview() throws E
855855
"1.2.3",
856856
"1.2.4",
857857
publisherId,
858-
Map.of(skill.getNamespaceId(), com.iflytek.skillhub.domain.namespace.NamespaceRole.OWNER)
858+
Map.of(skill.getNamespaceId(), com.iflytek.skillhub.domain.namespace.NamespaceRole.OWNER),
859+
false
859860
);
860861

861862
assertEquals("1.2.4", result.version().getVersion());
@@ -892,7 +893,8 @@ void testRereleasePublishedVersion_ShouldRejectDuplicateTargetVersion() throws E
892893
"1.2.3",
893894
"1.2.4",
894895
publisherId,
895-
Map.of(skill.getNamespaceId(), com.iflytek.skillhub.domain.namespace.NamespaceRole.OWNER)
896+
Map.of(skill.getNamespaceId(), com.iflytek.skillhub.domain.namespace.NamespaceRole.OWNER),
897+
false
896898
));
897899
}
898900

@@ -953,7 +955,8 @@ void testRereleasePublishedVersion_PrivateSkill_ShouldGoToUploaded() throws Exce
953955
"1.2.3",
954956
"1.2.4",
955957
publisherId,
956-
Map.of(skill.getNamespaceId(), com.iflytek.skillhub.domain.namespace.NamespaceRole.OWNER)
958+
Map.of(skill.getNamespaceId(), com.iflytek.skillhub.domain.namespace.NamespaceRole.OWNER),
959+
false
957960
);
958961

959962
assertEquals("1.2.4", result.version().getVersion());
@@ -966,6 +969,100 @@ void testRereleasePublishedVersion_PrivateSkill_ShouldGoToUploaded() throws Exce
966969
assertEquals(30L, skill.getLatestVersionId());
967970
}
968971

972+
@Test
973+
void testRereleasePublishedVersion_ShouldRequireConfirmationWhenWarningsExist() throws Exception {
974+
String publisherId = "user-100";
975+
Skill skill = new Skill(1L, "demo-skill", publisherId, SkillVisibility.PUBLIC);
976+
setId(skill, 11L);
977+
skill.setDisplayName("Demo Skill");
978+
skill.setSummary("Original summary");
979+
Namespace namespace = new Namespace("global", "Global", "owner");
980+
setId(namespace, 1L);
981+
982+
SkillVersion sourceVersion = new SkillVersion(skill.getId(), "1.2.3", publisherId);
983+
setId(sourceVersion, 21L);
984+
sourceVersion.setStatus(SkillVersionStatus.PUBLISHED);
985+
sourceVersion.setPublishedAt(Instant.parse("2026-03-15T10:00:00Z"));
986+
987+
String sourceSkillMd = "---\nname: Demo Skill\ndescription: Original summary\nversion: 1.2.3\n---\nHello world";
988+
SkillFile skillMdFile = new SkillFile(sourceVersion.getId(), "SKILL.md", (long) sourceSkillMd.getBytes(StandardCharsets.UTF_8).length, "text/markdown", "hash1", "skills/11/21/SKILL.md");
989+
SkillMetadata rereleaseMetadata = new SkillMetadata(
990+
"Demo Skill", "Original summary", "1.2.4", "Hello world",
991+
Map.of("name", "Demo Skill", "description", "Original summary", "version", "1.2.4"));
992+
993+
when(skillRepository.findById(skill.getId())).thenReturn(Optional.of(skill));
994+
when(namespaceRepository.findById(skill.getNamespaceId())).thenReturn(Optional.of(namespace));
995+
when(namespaceRepository.findBySlug("global")).thenReturn(Optional.of(namespace));
996+
when(skillVersionRepository.findBySkillIdAndVersion(skill.getId(), "1.2.3")).thenReturn(Optional.of(sourceVersion));
997+
when(skillVersionRepository.findBySkillIdAndVersion(skill.getId(), "1.2.4")).thenReturn(Optional.empty());
998+
when(skillFileRepository.findByVersionId(sourceVersion.getId())).thenReturn(List.of(skillMdFile));
999+
when(objectStorageService.getObject(skillMdFile.getStorageKey())).thenReturn(new java.io.ByteArrayInputStream(sourceSkillMd.getBytes(StandardCharsets.UTF_8)));
1000+
when(skillPackageValidator.validate(anyList())).thenReturn(ValidationResult.pass());
1001+
when(skillMetadataParser.parse(anyString())).thenReturn(rereleaseMetadata);
1002+
when(prePublishValidator.validate(any())).thenReturn(ValidationResult.warn(List.of(
1003+
"SKILL.md line 5 contains a value that looks like a secret or token.")));
1004+
1005+
DomainBadRequestException exception = assertThrows(DomainBadRequestException.class, () -> service.rereleasePublishedVersion(
1006+
skill.getId(), "1.2.3", "1.2.4", publisherId,
1007+
Map.of(skill.getNamespaceId(), com.iflytek.skillhub.domain.namespace.NamespaceRole.OWNER),
1008+
false
1009+
));
1010+
1011+
assertEquals("error.skill.publish.precheck.confirmRequired", exception.messageCode());
1012+
assertTrue(String.valueOf(exception.messageArgs()[0]).contains("looks like a secret or token"));
1013+
verify(skillVersionRepository, never()).save(any(SkillVersion.class));
1014+
}
1015+
1016+
@Test
1017+
void testRereleasePublishedVersion_ShouldSucceedWhenWarningsConfirmed() throws Exception {
1018+
String publisherId = "user-100";
1019+
Skill skill = new Skill(1L, "demo-skill", publisherId, SkillVisibility.PUBLIC);
1020+
setId(skill, 11L);
1021+
skill.setDisplayName("Demo Skill");
1022+
skill.setSummary("Original summary");
1023+
Namespace namespace = new Namespace("global", "Global", "owner");
1024+
setId(namespace, 1L);
1025+
1026+
SkillVersion sourceVersion = new SkillVersion(skill.getId(), "1.2.3", publisherId);
1027+
setId(sourceVersion, 21L);
1028+
sourceVersion.setStatus(SkillVersionStatus.PUBLISHED);
1029+
sourceVersion.setPublishedAt(Instant.parse("2026-03-15T10:00:00Z"));
1030+
1031+
String sourceSkillMd = "---\nname: Demo Skill\ndescription: Original summary\nversion: 1.2.3\n---\nHello world";
1032+
SkillFile skillMdFile = new SkillFile(sourceVersion.getId(), "SKILL.md", (long) sourceSkillMd.getBytes(StandardCharsets.UTF_8).length, "text/markdown", "hash1", "skills/11/21/SKILL.md");
1033+
SkillMetadata rereleaseMetadata = new SkillMetadata(
1034+
"Demo Skill", "Original summary", "1.2.4", "Hello world",
1035+
Map.of("name", "Demo Skill", "description", "Original summary", "version", "1.2.4"));
1036+
1037+
when(skillRepository.findById(skill.getId())).thenReturn(Optional.of(skill));
1038+
when(namespaceRepository.findById(skill.getNamespaceId())).thenReturn(Optional.of(namespace));
1039+
when(namespaceRepository.findBySlug("global")).thenReturn(Optional.of(namespace));
1040+
when(skillVersionRepository.findBySkillIdAndVersion(skill.getId(), "1.2.3")).thenReturn(Optional.of(sourceVersion));
1041+
when(skillVersionRepository.findBySkillIdAndVersion(skill.getId(), "1.2.4")).thenReturn(Optional.empty());
1042+
when(skillFileRepository.findByVersionId(sourceVersion.getId())).thenReturn(List.of(skillMdFile));
1043+
when(objectStorageService.getObject(skillMdFile.getStorageKey())).thenReturn(new java.io.ByteArrayInputStream(sourceSkillMd.getBytes(StandardCharsets.UTF_8)));
1044+
when(skillPackageValidator.validate(anyList())).thenReturn(ValidationResult.pass());
1045+
when(skillMetadataParser.parse(anyString())).thenReturn(rereleaseMetadata);
1046+
when(prePublishValidator.validate(any())).thenReturn(ValidationResult.warn(List.of(
1047+
"SKILL.md line 5 contains a value that looks like a secret or token.")));
1048+
when(skillVersionRepository.save(any(SkillVersion.class))).thenAnswer(invocation -> {
1049+
SkillVersion saved = invocation.getArgument(0);
1050+
if (saved.getId() == null) { setId(saved, 30L); }
1051+
return saved;
1052+
});
1053+
when(skillRepository.save(any())).thenReturn(skill);
1054+
1055+
SkillPublishService.PublishResult result = service.rereleasePublishedVersion(
1056+
skill.getId(), "1.2.3", "1.2.4", publisherId,
1057+
Map.of(skill.getNamespaceId(), com.iflytek.skillhub.domain.namespace.NamespaceRole.OWNER),
1058+
true // confirmWarnings = true → should bypass warning and succeed
1059+
);
1060+
1061+
assertEquals("1.2.4", result.version().getVersion());
1062+
assertEquals(SkillVersionStatus.PENDING_REVIEW, result.version().getStatus());
1063+
verify(skillVersionRepository, atLeastOnce()).save(any(SkillVersion.class));
1064+
}
1065+
9691066
@Test
9701067
void testPublishFromEntries_ShouldRejectWhenOtherOwnerHasPublishedSkill() throws Exception {
9711068
String namespaceSlug = "test-ns";
@@ -999,6 +1096,39 @@ void testPublishFromEntries_ShouldRejectWhenOtherOwnerHasPublishedSkill() throws
9991096
));
10001097
}
10011098

1099+
@Test
1100+
void testPublishFromEntries_ShouldRejectWithPrivateConflictWhenOtherOwnerHasPrivatePublishedSkill() throws Exception {
1101+
String namespaceSlug = "test-ns";
1102+
String publisherId = "user-200";
1103+
String skillMdContent = "---\nname: test-skill\ndescription: Test\nversion: 1.0.0\n---\nBody";
1104+
1105+
PackageEntry skillMd = new PackageEntry("SKILL.md", skillMdContent.getBytes(), skillMdContent.length(), "text/markdown");
1106+
List<PackageEntry> entries = List.of(skillMd);
1107+
1108+
Namespace namespace = new Namespace(namespaceSlug, "Test NS", "user-1");
1109+
setId(namespace, 1L);
1110+
NamespaceMember member = mock(NamespaceMember.class);
1111+
SkillMetadata metadata = new SkillMetadata("test-skill", "Test", "1.0.0", "Body", Map.of());
1112+
1113+
Skill existingSkill = new Skill(1L, "test-skill", "user-100", SkillVisibility.PRIVATE);
1114+
setId(existingSkill, 1L);
1115+
SkillVersion publishedVersion = new SkillVersion(1L, "0.1.0", "user-100");
1116+
publishedVersion.setStatus(SkillVersionStatus.PUBLISHED);
1117+
1118+
when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace));
1119+
when(namespaceMemberRepository.findByNamespaceIdAndUserId(any(), eq(publisherId))).thenReturn(Optional.of(member));
1120+
when(skillPackageValidator.validate(entries)).thenReturn(ValidationResult.pass());
1121+
when(skillMetadataParser.parse(skillMdContent)).thenReturn(metadata);
1122+
when(prePublishValidator.validate(any())).thenReturn(ValidationResult.pass());
1123+
when(skillRepository.findByNamespaceIdAndSlug(any(), eq("test-skill"))).thenReturn(List.of(existingSkill));
1124+
when(skillVersionRepository.findBySkillIdAndStatus(1L, SkillVersionStatus.PUBLISHED)).thenReturn(List.of(publishedVersion));
1125+
1126+
DomainBadRequestException ex = assertThrows(DomainBadRequestException.class, () -> service.publishFromEntries(
1127+
namespaceSlug, entries, publisherId, SkillVisibility.PRIVATE, Set.of()
1128+
));
1129+
assertEquals("error.skill.publish.nameConflict.private", ex.messageCode());
1130+
}
1131+
10021132
@Test
10031133
void testPublishFromEntries_ShouldAllowWhenOtherOwnerHasNonPublishedSkill() throws Exception {
10041134
String namespaceSlug = "test-ns";

web/src/api/client.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -488,14 +488,14 @@ export const skillLifecycleApi = {
488488
})
489489
},
490490

491-
async rereleaseVersion(namespace: string, slug: string, version: string, targetVersion: string): Promise<void> {
491+
async rereleaseVersion(namespace: string, slug: string, version: string, targetVersion: string, confirmWarnings = false): Promise<void> {
492492
const cleanNamespace = namespace.startsWith('@') ? namespace.slice(1) : namespace
493493
await fetchJson<void>(`${WEB_API_PREFIX}/skills/${cleanNamespace}/${encodeURIComponent(slug)}/versions/${encodeURIComponent(version)}/rerelease`, {
494494
method: 'POST',
495495
headers: await ensureCsrfHeaders({
496496
'Content-Type': 'application/json',
497497
}),
498-
body: JSON.stringify({ targetVersion }),
498+
body: JSON.stringify({ targetVersion, confirmWarnings }),
499499
})
500500
},
501501

web/src/api/generated/schema.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3237,6 +3237,7 @@ export interface components {
32373237
};
32383238
SkillVersionRereleaseRequest: {
32393239
targetVersion: string;
3240+
confirmWarnings?: boolean;
32403241
};
32413242
SkillReportSubmitRequest: {
32423243
reason?: string;

web/src/i18n/locales/en.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -911,6 +911,9 @@
911911
"rereleaseSuccessTitle": "Version re-released",
912912
"rereleaseSuccessDescription": "Created v{{target}} from v{{source}}.",
913913
"rereleaseErrorTitle": "Failed to re-release version",
914+
"rereleaseWarningTitle": "Pre-publish warning",
915+
"rereleaseWarningDescription": "We found the following risk reminders. If you understand them and still want to proceed, you can continue re-releasing.",
916+
"rereleaseWarningConfirm": "Continue re-releasing",
914917
"yankVersion": "Yank Current Version",
915918
"promoteToGlobal": "Promote to Global",
916919
"promotionSectionTitle": "Promote to Global",

web/src/i18n/locales/zh.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -912,6 +912,9 @@
912912
"rereleaseSuccessTitle": "版本已重新发布",
913913
"rereleaseSuccessDescription": "已基于 v{{source}} 创建新版本 v{{target}}。",
914914
"rereleaseErrorTitle": "重新发布版本失败",
915+
"rereleaseWarningTitle": "发布前风险提醒",
916+
"rereleaseWarningDescription": "检测到以下风险项。若你确认这些内容可以接受,仍可继续重新发布。",
917+
"rereleaseWarningConfirm": "继续重新发布",
915918
"yankVersion": "撤回当前版本",
916919
"promoteToGlobal": "申请提升到全局",
917920
"promotionSectionTitle": "提升到全局",

0 commit comments

Comments
 (0)