Skip to content

Commit 82b076a

Browse files
authored
Support for automatically generating SemVer branches (#5000)
* Added support for automatically generating SemVer branches * Revert a temporary snapshot version in root pom.xml * Fix a broken UI integration test
1 parent 930434e commit 82b076a

File tree

18 files changed

+337
-55
lines changed

18 files changed

+337
-55
lines changed

app/pom.xml

+9-10
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,6 @@
136136
<groupId>io.quarkus</groupId>
137137
<artifactId>quarkus-logging-json</artifactId>
138138
</dependency>
139-
140139
<dependency>
141140
<groupId>io.quarkus</groupId>
142141
<artifactId>quarkus-smallrye-fault-tolerance</artifactId>
@@ -160,21 +159,23 @@
160159
<artifactId>quarkus-jdbc-mssql</artifactId>
161160
</dependency>
162161

162+
<!-- Third Party Libraries -->
163+
<dependency>
164+
<groupId>io.strimzi</groupId>
165+
<artifactId>kafka-oauth-client</artifactId>
166+
</dependency>
163167
<dependency>
164168
<groupId>org.eclipse.jgit</groupId>
165169
<artifactId>org.eclipse.jgit</artifactId>
166170
</dependency>
167-
168171
<dependency>
169-
<groupId>commons-io</groupId>
170-
<artifactId>commons-io</artifactId>
172+
<groupId>org.semver4j</groupId>
173+
<artifactId>semver4j</artifactId>
171174
</dependency>
172175
<dependency>
173-
<groupId>io.strimzi</groupId>
174-
<artifactId>kafka-oauth-client</artifactId>
176+
<groupId>commons-io</groupId>
177+
<artifactId>commons-io</artifactId>
175178
</dependency>
176-
177-
<!-- Third Party Libraries -->
178179
<dependency>
179180
<groupId>commons-codec</groupId>
180181
<artifactId>commons-codec</artifactId>
@@ -198,13 +199,11 @@
198199
</exclusion>
199200
</exclusions>
200201
</dependency>
201-
202202
<dependency>
203203
<groupId>org.yaml</groupId>
204204
<artifactId>snakeyaml</artifactId>
205205
<version>${snakeyaml.version}</version>
206206
</dependency>
207-
208207
<dependency>
209208
<groupId>com.google.guava</groupId>
210209
<artifactId>guava</artifactId>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package io.apicurio.registry.semver;
2+
3+
import io.apicurio.common.apps.config.Dynamic;
4+
import io.apicurio.common.apps.config.Info;
5+
import jakarta.inject.Singleton;
6+
import org.eclipse.microprofile.config.inject.ConfigProperty;
7+
8+
import java.util.function.Supplier;
9+
10+
@Singleton
11+
public class SemVerConfigProperties {
12+
13+
@Dynamic(label = "Ensure all version numbers are 'semver' compatible", description = "When enabled, validate that all artifact versions conform to Semantic Versioning 2 format (https://semver.org).")
14+
@ConfigProperty(name = "apicurio.semver.validation.enabled", defaultValue = "false")
15+
@Info(category = "semver", description = "Validate that all artifact versions conform to Semantic Versioning 2 format (https://semver.org).", availableSince = "3.0.0")
16+
public Supplier<Boolean> validationEnabled;
17+
18+
@Dynamic(label = "Automatically create semver branches", description = "When enabled, automatically create or update branches for major ('A.x') and minor ('A.B.x') artifact versions.")
19+
@ConfigProperty(name = "apicurio.semver.branching.enabled", defaultValue = "false")
20+
@Info(category = "semver", description = "Automatically create or update branches for major ('A.x') and minor ('A.B.x') artifact versions.", availableSince = "3.0.0")
21+
public Supplier<Boolean> branchingEnabled;
22+
23+
@Dynamic(label = "Coerce invalid semver versions", description = "When enabled and automatically creating semver branches, invalid versions will be coerced to Semantic Versioning 2 format (https://semver.org) if possible.", requires = "apicurio.semver.branching.enabled=true")
24+
@ConfigProperty(name = "apicurio.semver.branching.coerce", defaultValue = "false")
25+
@Info(category = "semver", description = "If true, invalid versions will be coerced to Semantic Versioning 2 format (https://semver.org) if possible.", availableSince = "3.0.0")
26+
public Supplier<Boolean> coerceInvalidVersions;
27+
28+
}

app/src/main/java/io/apicurio/registry/storage/impl/sql/AbstractHandleFactory.java

+33-24
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,12 @@ protected void initialize(AgroalDataSource dataSource, String dataSourceId, Logg
3232
public <R, X extends Exception> R withHandle(HandleCallback<R, X> callback) throws X {
3333
LocalState state = state();
3434
try {
35-
// Create a new handle, or throw if one already exists (only one handle allowed at a time)
35+
// Create a new handle if necessary. Increment the "level" if a handle already exists.
3636
if (state.handle == null) {
3737
state.handle = new HandleImpl(dataSource.getConnection());
38+
state.level = 0;
3839
} else {
39-
throw new RegistryStorageException("Attempt to acquire a nested DB Handle.");
40+
state.level++;
4041
}
4142

4243
// Invoke the callback with the handle. This will either return a value (success)
@@ -54,32 +55,39 @@ public <R, X extends Exception> R withHandle(HandleCallback<R, X> callback) thro
5455
}
5556
throw e;
5657
} finally {
57-
// Commit or rollback the transaction
58-
try {
59-
if (state.handle != null) {
60-
if (state.handle.isRollback()) {
61-
log.trace("Rollback: {} #{}", state.handle.getConnection(),
62-
state.handle.getConnection().hashCode());
63-
state.handle.getConnection().rollback();
64-
} else {
65-
log.trace("Commit: {} #{}", state.handle.getConnection(),
66-
state.handle.getConnection().hashCode());
67-
state().handle.getConnection().commit();
58+
if (state.level > 0) {
59+
log.trace("Exiting nested call (level {}): {} #{}", state().level,
60+
state().handle.getConnection(), state().handle.getConnection().hashCode());
61+
state.level--;
62+
} else {
63+
// Commit or rollback the transaction
64+
try {
65+
if (state.handle != null) {
66+
if (state.handle.isRollback()) {
67+
log.trace("Rollback: {} #{}", state.handle.getConnection(),
68+
state.handle.getConnection().hashCode());
69+
state.handle.getConnection().rollback();
70+
} else {
71+
log.trace("Commit: {} #{}", state.handle.getConnection(),
72+
state.handle.getConnection().hashCode());
73+
state().handle.getConnection().commit();
74+
}
6875
}
76+
} catch (Exception e) {
77+
log.error("Could not release database connection/transaction", e);
6978
}
70-
} catch (Exception e) {
71-
log.error("Could not release database connection/transaction", e);
72-
}
7379

74-
// Close the connection
75-
try {
76-
if (state.handle != null) {
77-
state.handle.close();
78-
state.handle = null;
80+
// Close the connection
81+
try {
82+
if (state.handle != null) {
83+
state.handle.close();
84+
state.handle = null;
85+
state.level = 0;
86+
}
87+
} catch (Exception ex) {
88+
// Nothing we can do
89+
log.error("Could not close a database connection.", ex);
7990
}
80-
} catch (Exception ex) {
81-
// Nothing we can do
82-
log.error("Could not close a database connection.", ex);
8391
}
8492
}
8593
}
@@ -109,5 +117,6 @@ private LocalState state() {
109117

110118
private static class LocalState {
111119
HandleImpl handle;
120+
int level = 0;
112121
}
113122
}

app/src/main/java/io/apicurio/registry/storage/impl/sql/AbstractSqlRegistryStorage.java

+76-4
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import io.apicurio.registry.model.GA;
1212
import io.apicurio.registry.model.GAV;
1313
import io.apicurio.registry.model.VersionId;
14+
import io.apicurio.registry.semver.SemVerConfigProperties;
1415
import io.apicurio.registry.storage.RegistryStorage;
1516
import io.apicurio.registry.storage.StorageBehaviorProperties;
1617
import io.apicurio.registry.storage.StorageEvent;
@@ -113,9 +114,11 @@
113114
import io.quarkus.security.identity.SecurityIdentity;
114115
import jakarta.enterprise.event.Event;
115116
import jakarta.inject.Inject;
117+
import jakarta.validation.ValidationException;
116118
import org.apache.commons.lang3.tuple.ImmutablePair;
117119
import org.apache.commons.lang3.tuple.Pair;
118120
import org.eclipse.microprofile.config.inject.ConfigProperty;
121+
import org.semver4j.Semver;
119122
import org.slf4j.Logger;
120123

121124
import java.sql.ResultSet;
@@ -187,6 +190,9 @@ public abstract class AbstractSqlRegistryStorage implements RegistryStorage {
187190
@Inject
188191
RegistryStorageContentUtils utils;
189192

193+
@Inject
194+
SemVerConfigProperties semVerConfigProps;
195+
190196
protected SqlStatements sqlStatements() {
191197
return sqlStatements;
192198
}
@@ -554,7 +560,6 @@ private ArtifactVersionMetaDataDto createArtifactVersionRaw(Handle handle, boole
554560
.bind(12, contentId).execute();
555561

556562
gav = new GAV(groupId, artifactId, finalVersion1);
557-
createOrUpdateBranchRaw(handle, gav, BranchId.LATEST, true);
558563
} else {
559564
handle.createUpdate(sqlStatements.insertVersion(false)).bind(0, globalId)
560565
.bind(1, normalizeGroupId(groupId)).bind(2, artifactId).bind(3, version)
@@ -571,7 +576,6 @@ private ArtifactVersionMetaDataDto createArtifactVersionRaw(Handle handle, boole
571576
}
572577

573578
gav = getGAVByGlobalId(handle, globalId);
574-
createOrUpdateBranchRaw(handle, gav, BranchId.LATEST, true);
575579
}
576580

577581
// Insert labels into the "version_labels" table
@@ -583,6 +587,10 @@ private ArtifactVersionMetaDataDto createArtifactVersionRaw(Handle handle, boole
583587
});
584588
}
585589

590+
// Update system generated branches
591+
createOrUpdateBranchRaw(handle, gav, BranchId.LATEST, true);
592+
createOrUpdateSemverBranches(handle, gav);
593+
586594
// Create any user defined branches
587595
if (branches != null && !branches.isEmpty()) {
588596
branches.forEach(branch -> {
@@ -595,6 +603,54 @@ private ArtifactVersionMetaDataDto createArtifactVersionRaw(Handle handle, boole
595603
.map(ArtifactVersionMetaDataDtoMapper.instance).one();
596604
}
597605

606+
/**
607+
* If SemVer support is enabled, create (or update) the automatic system generated semantic versioning
608+
* branches.
609+
*
610+
* @param handle
611+
* @param gav
612+
*/
613+
private void createOrUpdateSemverBranches(Handle handle, GAV gav) {
614+
boolean validationEnabled = semVerConfigProps.validationEnabled.get();
615+
boolean branchingEnabled = semVerConfigProps.branchingEnabled.get();
616+
boolean coerceInvalidVersions = semVerConfigProps.coerceInvalidVersions.get();
617+
618+
// Validate the version if validation is enabled.
619+
if (validationEnabled) {
620+
Semver semver = Semver.parse(gav.getRawVersionId());
621+
if (semver == null) {
622+
throw new ValidationException("Version '" + gav.getRawVersionId()
623+
+ "' does not conform to Semantic Versioning 2 format.");
624+
}
625+
}
626+
627+
// Create branches if branching is enabled
628+
if (!branchingEnabled) {
629+
return;
630+
}
631+
632+
Semver semver = null;
633+
if (coerceInvalidVersions) {
634+
semver = Semver.coerce(gav.getRawVersionId());
635+
if (semver == null) {
636+
throw new ValidationException("Version '" + gav.getRawVersionId()
637+
+ "' cannot be coerced to Semantic Versioning 2 format.");
638+
}
639+
} else {
640+
semver = Semver.parse(gav.getRawVersionId());
641+
if (semver == null) {
642+
throw new ValidationException("Version '" + gav.getRawVersionId()
643+
+ "' does not conform to Semantic Versioning 2 format.");
644+
}
645+
}
646+
if (semver == null) {
647+
throw new UnreachableCodeException("Unexpectedly reached unreachable code!");
648+
}
649+
createOrUpdateBranchRaw(handle, gav, new BranchId(semver.getMajor() + ".x"), true);
650+
createOrUpdateBranchRaw(handle, gav, new BranchId(semver.getMajor() + "." + semver.getMinor() + ".x"),
651+
true);
652+
}
653+
598654
/**
599655
* Store the content in the database and return the content ID of the new row. If the content already
600656
* exists, just return the content ID of the existing row.
@@ -3031,6 +3087,11 @@ public BranchMetaDataDto createBranch(GA ga, BranchId branchId, String descripti
30313087

30323088
@Override
30333089
public void updateBranchMetaData(GA ga, BranchId branchId, EditableBranchMetaDataDto dto) {
3090+
BranchMetaDataDto bmd = getBranchMetaData(ga, branchId);
3091+
if (bmd.isSystemDefined()) {
3092+
throw new NotAllowedException("System generated branches cannot be modified.");
3093+
}
3094+
30343095
String modifiedBy = securityIdentity.getPrincipal().getName();
30353096
Date modifiedOn = new Date();
30363097
log.debug("Updating metadata for branch {} of {}/{}.", branchId, ga.getRawGroupIdWithNull(),
@@ -3220,6 +3281,11 @@ public VersionSearchResultsDto getBranchVersions(GA ga, BranchId branchId, int o
32203281

32213282
@Override
32223283
public void appendVersionToBranch(GA ga, BranchId branchId, VersionId version) {
3284+
BranchMetaDataDto bmd = getBranchMetaData(ga, branchId);
3285+
if (bmd.isSystemDefined()) {
3286+
throw new NotAllowedException("System generated branches cannot be modified.");
3287+
}
3288+
32233289
try {
32243290
handles.withHandle(handle -> {
32253291
appendVersionToBranchRaw(handle, ga, branchId, version);
@@ -3257,6 +3323,11 @@ private void appendVersionToBranchRaw(Handle handle, GA ga, BranchId branchId, V
32573323

32583324
@Override
32593325
public void replaceBranchVersions(GA ga, BranchId branchId, List<VersionId> versions) {
3326+
BranchMetaDataDto bmd = getBranchMetaData(ga, branchId);
3327+
if (bmd.isSystemDefined()) {
3328+
throw new NotAllowedException("System generated branches cannot be modified.");
3329+
}
3330+
32603331
handles.withHandle(handle -> {
32613332
// Delete all previous versions.
32623333
handle.createUpdate(sqlStatements.deleteBranchVersions()).bind(0, ga.getRawGroupId())
@@ -3341,8 +3412,9 @@ private GAV getGAVByGlobalId(Handle handle, long globalId) {
33413412

33423413
@Override
33433414
public void deleteBranch(GA ga, BranchId branchId) {
3344-
if (BranchId.LATEST.equals(branchId)) {
3345-
throw new NotAllowedException("Artifact branch 'latest' cannot be deleted.");
3415+
BranchMetaDataDto bmd = getBranchMetaData(ga, branchId);
3416+
if (bmd.isSystemDefined()) {
3417+
throw new NotAllowedException("System generated branches cannot be deleted.");
33463418
}
33473419

33483420
handles.withHandleNoException(handle -> {

app/src/main/resources/application.properties

+3-1
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,9 @@ apicurio.authn.basic-client-credentials.enabled.dynamic.allow=${apicurio.config.
127127
apicurio.rest.deletion.group.enabled.dynamic.allow=${apicurio.config.dynamic.allow-all}
128128
apicurio.rest.deletion.artifact.enabled.dynamic.allow=${apicurio.config.dynamic.allow-all}
129129
apicurio.rest.deletion.artifactVersion.enabled.dynamic.allow=${apicurio.config.dynamic.allow-all}
130-
130+
apicurio.semver.validation.enabled.dynamic.allow=${apicurio.config.dynamic.allow-all}
131+
apicurio.semver.branching.enabled.dynamic.allow=${apicurio.config.dynamic.allow-all}
132+
apicurio.semver.branching.coerce.dynamic.allow=${apicurio.config.dynamic.allow-all}
131133

132134
# Error
133135
apicurio.api.errors.include-stack-in-response=false

app/src/test/java/io/apicurio/registry/noprofile/rest/v3/BranchesTest.java

+8
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import io.apicurio.registry.rest.client.models.CreateBranch;
88
import io.apicurio.registry.rest.client.models.CreateVersion;
99
import io.apicurio.registry.rest.client.models.EditableBranchMetaData;
10+
import io.apicurio.registry.rest.client.models.Error;
1011
import io.apicurio.registry.rest.client.models.ReplaceBranchVersions;
1112
import io.apicurio.registry.rest.client.models.VersionMetaData;
1213
import io.apicurio.registry.rest.client.models.VersionSearchResults;
@@ -39,6 +40,13 @@ public void testLatestBranch() throws Exception {
3940
VersionSearchResults versions = clientV3.groups().byGroupId(groupId).artifacts()
4041
.byArtifactId(artifactId).branches().byBranchId("latest").versions().get();
4142
Assertions.assertEquals(2, versions.getCount());
43+
44+
// Not allowed to delete the latest branch.
45+
var error = Assertions.assertThrows(Error.class, () -> {
46+
clientV3.groups().byGroupId(groupId).artifacts().byArtifactId(artifactId).branches()
47+
.byBranchId("latest").delete();
48+
});
49+
Assertions.assertEquals("System generated branches cannot be deleted.", error.getMessageEscaped());
4250
}
4351

4452
@Test

0 commit comments

Comments
 (0)