diff --git a/fcli-core/fcli-app/src/main/java/com/fortify/cli/app/runner/util/FortifyCLIStaticInitializer.java b/fcli-core/fcli-app/src/main/java/com/fortify/cli/app/runner/util/FortifyCLIStaticInitializer.java index 5368326623..4351ce651e 100644 --- a/fcli-core/fcli-app/src/main/java/com/fortify/cli/app/runner/util/FortifyCLIStaticInitializer.java +++ b/fcli-core/fcli-app/src/main/java/com/fortify/cli/app/runner/util/FortifyCLIStaticInitializer.java @@ -22,7 +22,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import com.fortify.cli.common.action.helper.ActionSchemaVersionHelper; +import com.fortify.cli.common.action.helper.ActionSchemaHelper; import com.fortify.cli.common.http.ssl.truststore.helper.TrustStoreConfigDescriptor; import com.fortify.cli.common.http.ssl.truststore.helper.TrustStoreConfigHelper; import com.fortify.cli.common.i18n.helper.LanguageHelper; @@ -84,7 +84,7 @@ private void initializeSSCProperties() { } private void initializeActionProperties() { - System.setProperty("fcli.action.supportedSchemaVersions", ActionSchemaVersionHelper.getSupportedSchemaVersions().stream().collect(Collectors.joining(", "))); + System.setProperty("fcli.action.supportedSchemaVersions", ActionSchemaHelper.getSupportedSchemaVersionsString()); } private void initializeTrustStore() { diff --git a/fcli-core/fcli-common/build.gradle b/fcli-core/fcli-common/build.gradle index 3cdaa39cce..f4a7a793e2 100644 --- a/fcli-core/fcli-common/build.gradle +++ b/fcli-core/fcli-common/build.gradle @@ -5,7 +5,7 @@ task zipResources_templates(type: Zip) { // TODO We should also sign file; how do we invoke a sign operation from Gradle? filter(line->project.version.startsWith('0.') ? line - : line.replaceAll("https://fortify.github.io/fcli/schemas/action/fcli-action-schema-dev.json", "https://fortify.github.io/fcli/schemas/action/fcli-action-schema-${project.version}.json")) + : line.replaceAll("https://fortify.github.io/fcli/schemas/action/fcli-action-schema-dev.json", "https://fortify.github.io/fcli/schemas/action/fcli-action-schema-${fcliActionSchemaVersion}.json")) } } @@ -21,6 +21,7 @@ task generateFcliBuildProperties { entry(key: "projectName", value: "fcli") entry(key: "projectVersion", value: project.version) entry(key: "buildDate", value: buildTime.format('yyyy-MM-dd HH:mm:ss')) + entry(key: "actionSchemaVersion", value: fcliActionSchemaVersion) } def resourceConfigOutputDir = "${buildPropertiesDir}/META-INF/native-image/fcli-build-properties" mkdir "${resourceConfigOutputDir}" diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/helper/ActionLoaderHelper.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/helper/ActionLoaderHelper.java index 09ee621027..fa6e4ddf65 100644 --- a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/helper/ActionLoaderHelper.java +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/helper/ActionLoaderHelper.java @@ -261,8 +261,8 @@ private final String updateSchema(String actionText) { } else { schemaUri = propertyValue; } - var schemaVersion = ActionSchemaVersionHelper.getSchemaVersion(schemaUri); - if ( !ActionSchemaVersionHelper.isSupportedSchemaVersion(schemaVersion) ) { + var schemaVersion = ActionSchemaHelper.getSchemaVersion(schemaUri); + if ( !ActionSchemaHelper.isSupportedSchemaVersion(schemaVersion) ) { LOG.warn("WARN: Action was designed for fcli version "+schemaVersion+" and may fail"); } return result; diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/helper/ActionSchemaVersionHelper.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/helper/ActionSchemaHelper.java similarity index 70% rename from fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/helper/ActionSchemaVersionHelper.java rename to fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/helper/ActionSchemaHelper.java index 0e675d84ce..2ce5587132 100644 --- a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/helper/ActionSchemaVersionHelper.java +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/helper/ActionSchemaHelper.java @@ -14,14 +14,15 @@ import java.text.MessageFormat; import java.text.ParseException; -import java.util.Arrays; -import java.util.List; import com.formkiq.graalvm.annotations.Reflectable; import com.fortify.cli.common.util.FcliBuildPropertiesHelper; +import com.fortify.cli.common.util.SemVer; -@Reflectable public final class ActionSchemaVersionHelper { +@Reflectable public final class ActionSchemaHelper { private static final MessageFormat URI_FORMAT = new MessageFormat("https://fortify.github.io/fcli/schemas/action/fcli-action-schema-{0}.json"); + private static final boolean IS_FCLI_DEV_RELEASE = FcliBuildPropertiesHelper.isDevelopmentRelease(); + private static final SemVer CURRENT_SCHEMA_VERSION = new SemVer(FcliBuildPropertiesHelper.getFcliActionSchemaVersion()); /** Get the schema URI for the current enum entry by formatting schema version as URI */ public static final String toURI(String version) { @@ -43,11 +44,14 @@ public static final String getSchemaVersion(String schemaURI) { /** Check whether given schema version is supported */ public static final boolean isSupportedSchemaVersion(String version) { - return getSupportedSchemaVersions().contains(version); + return IS_FCLI_DEV_RELEASE + ? true + : CURRENT_SCHEMA_VERSION.isCompatibleWith(version); } - public static final List getSupportedSchemaVersions() { - var fcliVersion = FcliBuildPropertiesHelper.getFcliVersion(); - return Arrays.asList(fcliVersion.startsWith("0.")?"dev":fcliVersion); + public static final String getSupportedSchemaVersionsString() { + return IS_FCLI_DEV_RELEASE + ? "any (as this is an fcli development version)" + : CURRENT_SCHEMA_VERSION.getCompatibleVersionsString().orElse("unknown"); } } \ No newline at end of file diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/util/FcliBuildPropertiesHelper.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/util/FcliBuildPropertiesHelper.java index 81d6a99f12..da3671c328 100644 --- a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/util/FcliBuildPropertiesHelper.java +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/util/FcliBuildPropertiesHelper.java @@ -27,6 +27,11 @@ public static final Properties getBuildProperties() { return buildProperties; } + public static final boolean isDevelopmentRelease() { + var version = getFcliVersion(); + return version.startsWith("0.") || version.equals("unknown"); + } + public static final String getFcliProjectName() { return buildProperties.getProperty("projectName", "fcli"); } @@ -49,6 +54,10 @@ public static final Date getFcliBuildDate() { return null; } + public static final String getFcliActionSchemaVersion() { + return buildProperties.getProperty("actionSchemaVersion", "unknown"); + } + public static final String getFcliBuildInfo() { return String.format("%s version %s, built on %s" , FcliBuildPropertiesHelper.getFcliProjectName() diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/util/SemVer.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/util/SemVer.java new file mode 100644 index 0000000000..36845e12b3 --- /dev/null +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/util/SemVer.java @@ -0,0 +1,153 @@ +/** + * Copyright 2023 Open Text. + * + * The only warranties for products and services of Open Text + * and its affiliates and licensors ("Open Text") are as may + * be set forth in the express warranty statements accompanying + * such products and services. Nothing herein should be construed + * as constituting an additional warranty. Open Text shall not be + * liable for technical or editorial errors or omissions contained + * herein. The information contained herein is subject to change + * without notice. + */ +package com.fortify.cli.common.util; + +import java.lang.module.ModuleDescriptor.Version; +import java.text.MessageFormat; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import lombok.Getter; + +/** + * This class represents an optional semantic version, based on the + * version string passed to the constructor. This version string doesn't + * need to be a proper semantic version, in which case most methods will + * return an empty {@link Optional}, and comparison methods regard a + * non-proper semantic version as not equal to/less than a true semantic + * version. + * + * @author Ruud Senden + */ +@Getter +public final class SemVer { + private static final Pattern semverPattern = Pattern.compile("([1-9]\\d*)\\.(\\d+)\\.(\\d+)(?:-(.*))?"); + private final String semver; + private final boolean properSemver; + private final Optional major; + private final Optional minor; + private final Optional patch; + private final Optional label; + private final Optional majorMinor; + private final Optional majorMinorPatch; + private final Optional version; + + public SemVer(String semver) { + this.semver = semver==null?"":semver; + var matcher = semverPattern.matcher(this.semver); + this.properSemver = matcher.matches(); + this.major = optionalFormat(matcher, "{1}"); + this.minor = optionalFormat(matcher, "{2}"); + this.patch = optionalFormat(matcher, "{3}"); + this.label = optionalFormat(matcher, "{4}"); + this.majorMinor = optionalFormat(matcher, "{1}.{2}"); + this.majorMinorPatch = optionalFormat(matcher, "{1}.{2}.{3}"); + this.version = Optional.ofNullable(!matcher.matches() ? null : Version.parse(semver)); + } + + /** + * @return Minimal compatible version, i.e., major.0.0 + */ + public final Optional getMinimalCompatible() { + return major.map(_major->String.format("%s.0.0", _major)); + } + + /** + * @return Maximal compatible version, i.e., major.minor.* + */ + public final Optional getMaximalCompatibleString() { + return majorMinor.map(_majorMinor->String.format("%s.*", _majorMinor)); + } + + /** + * @return String describing minimal and maximal compatible versions + */ + public final Optional getCompatibleVersionsString() { + var _minCompat = getMinimalCompatible(); + var _maxCompat = getMaximalCompatibleString(); + return _minCompat.equals(getMajorMinorPatch()) + ? _maxCompat + : Optional.ofNullable(!properSemver + ? null + : String.format("%s-%s", _minCompat.get(), _maxCompat.get())); + } + + /** + * + * @param other semver string to compare + * @return true, unless either this or other isn't a proper semver, or if major + * version is different, or if this minor is less than other minor + */ + public final boolean isCompatibleWith(String other) { + return isCompatibleWith(new SemVer(other)); + } + + /** + * @param other semver to compare + * @return true, unless either this or other isn't a proper semver, or if major + * version is different, or if this minor is less than other minor + */ + public final boolean isCompatibleWith(SemVer other) { + if ( !this.properSemver || !other.properSemver ) { return false; } + if ( !this.major.equals(other.major) ) { return false; } + if ( Integer.parseInt(this.minor.orElseThrow()) optionalFormat(Matcher matcher, String format) { + return Optional.ofNullable(!matcher.matches() + ? null + : new MessageFormat(format).format(new Object[] { + matcher.group(0), matcher.group(1), matcher.group(2), matcher.group(3), matcher.group(4) + })); + } +} diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/util/SemVerHelper.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/util/SemVerHelper.java deleted file mode 100644 index 28eb3e1a1c..0000000000 --- a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/util/SemVerHelper.java +++ /dev/null @@ -1,60 +0,0 @@ -/** - * Copyright 2023 Open Text. - * - * The only warranties for products and services of Open Text - * and its affiliates and licensors ("Open Text") are as may - * be set forth in the express warranty statements accompanying - * such products and services. Nothing herein should be construed - * as constituting an additional warranty. Open Text shall not be - * liable for technical or editorial errors or omissions contained - * herein. The information contained herein is subject to change - * without notice. - */ -package com.fortify.cli.common.util; - -import java.lang.module.ModuleDescriptor.Version; -import java.util.Optional; -import java.util.regex.Pattern; - -public final class SemVerHelper { - private static final Pattern semverPattern = Pattern.compile("([1-9]\\d*)\\.(\\d+)\\.(\\d+)(?:-(.*))?"); - /** - * Loosely compare two semantic versions, returning -1 if first semver is lower than - * the second, 0 if they are the same, or 1 if first semver is higher than the - * second. Null, blank, or non-semver values are always considered lower than - * true semvers. - * - * @param semver1 - * @param semver2 - * @return - */ - public static final int compare(String semver1, String semver2) { - var semver1Matcher = semverPattern.matcher(semver1==null?"":semver1); - var semver2Matcher = semverPattern.matcher(semver2==null?"":semver2); - if ( (semver1==null && semver2==null) || semver1.equals(semver2) ) { - return 0; - } else if ( !semver1Matcher.matches() && !semver2Matcher.matches() ) { - return semver1.compareTo(semver2); - } else if ( semver1Matcher.matches() && !semver2Matcher.matches() ) { - return 1; - } else if ( !semver1Matcher.matches() && semver2Matcher.matches() ) { - return -1; - } else { - var version1 = Version.parse(semver1); - var version2 = Version.parse(semver2); - return version1.compareTo(version2); - } - } - - public static final Optional getMajor(String semver) { - var matcher = semverPattern.matcher(semver); - return !matcher.matches() ? Optional.empty() : Optional.of(matcher.group(1)); - } - - public static final Optional getMajorMinor(String semver) { - var matcher = semverPattern.matcher(semver); - return !matcher.matches() - ? Optional.empty() - : Optional.of(String.format("%s.%s", matcher.group(1), matcher.group(2))); - } -} diff --git a/fcli-core/fcli-common/src/test/java/com/fortify/cli/common/util/SemVerHelperTest.java b/fcli-core/fcli-common/src/test/java/com/fortify/cli/common/util/SemVerTest.java similarity index 56% rename from fcli-core/fcli-common/src/test/java/com/fortify/cli/common/util/SemVerHelperTest.java rename to fcli-core/fcli-common/src/test/java/com/fortify/cli/common/util/SemVerTest.java index 531660a765..e33f56f51f 100644 --- a/fcli-core/fcli-common/src/test/java/com/fortify/cli/common/util/SemVerHelperTest.java +++ b/fcli-core/fcli-common/src/test/java/com/fortify/cli/common/util/SemVerTest.java @@ -21,7 +21,7 @@ * * @author Ruud Senden */ -public class SemVerHelperTest { +public class SemVerTest { @ParameterizedTest @CsvSource({ ",,0", @@ -43,6 +43,35 @@ public class SemVerHelperTest { "1.2.0-alpha1,1.2.0,-1" }) public void testSemVerCompare(String semver1, String semver2, int expectedResult) throws Exception { - assertEquals(expectedResult, SemVerHelper.compare(semver1, semver2)); + assertEquals(expectedResult, new SemVer(semver1).compareTo(semver2)); + } + + @ParameterizedTest + @CsvSource({ + ",unknown", + "a,unknown", + "1.2.3,1.0.0-1.2.*", + "1.2.3-alpha1,1.0.0-1.2.*", + "2.0.0,2.0.*" + }) + public void testSemVerCompatibleVersionsString(String semver, String expectedResult) throws Exception { + assertEquals(expectedResult, new SemVer(semver).getCompatibleVersionsString().orElse("unknown")); + } + + @ParameterizedTest + @CsvSource({ + ",,false", + "1.2.3,,false", + ",1.2.3,false", + "1.2.3,1.2.3,true", + "1.2.3,1.2.0,true", + "1.2.3,1.2.5,true", + "2.0.0,1.2.3,false", + "1.2.3,2.0.0,false", + "1.2.3,1.4.0,false", + "1.4.0,1.2.3,true" + }) + public void testSemVerIsCompatibleWith(String semver1, String semver2, boolean expectedResult) throws Exception { + assertEquals(expectedResult, new SemVer(semver1).isCompatibleWith(semver2)); } } diff --git a/fcli-core/fcli-fod/build.gradle b/fcli-core/fcli-fod/build.gradle index c35dc77506..3e3b71fcca 100644 --- a/fcli-core/fcli-fod/build.gradle +++ b/fcli-core/fcli-fod/build.gradle @@ -5,7 +5,7 @@ task zipResources_actions(type: Zip) { // TODO We should also sign file; how do we invoke a sign operation from Gradle? filter(line->project.version.startsWith('0.') ? line - : line.replaceAll("https://fortify.github.io/fcli/schemas/action/fcli-action-schema-dev.json", "https://fortify.github.io/fcli/schemas/action/fcli-action-schema-${project.version}.json")) + : line.replaceAll("https://fortify.github.io/fcli/schemas/action/fcli-action-schema-dev.json", "https://fortify.github.io/fcli/schemas/action/fcli-action-schema-${fcliActionSchemaVersion}.json")) } include '*.yaml' } diff --git a/fcli-core/fcli-ssc/build.gradle b/fcli-core/fcli-ssc/build.gradle index fd65db8546..fa779bbf00 100644 --- a/fcli-core/fcli-ssc/build.gradle +++ b/fcli-core/fcli-ssc/build.gradle @@ -5,7 +5,7 @@ task zipResources_actions(type: Zip) { // TODO We should also sign file; how do we invoke a sign operation from Gradle? filter(line->project.version.startsWith('0.') ? line - : line.replaceAll("https://fortify.github.io/fcli/schemas/action/fcli-action-schema-dev.json", "https://fortify.github.io/fcli/schemas/action/fcli-action-schema-${project.version}.json")) + : line.replaceAll("https://fortify.github.io/fcli/schemas/action/fcli-action-schema-dev.json", "https://fortify.github.io/fcli/schemas/action/fcli-action-schema-${fcliActionSchemaVersion}.json")) } include '*.yaml' } diff --git a/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/_common/helper/ToolInstallationHelper.java b/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/_common/helper/ToolInstallationHelper.java index 6db7e2efb2..c584ccd59d 100644 --- a/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/_common/helper/ToolInstallationHelper.java +++ b/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/_common/helper/ToolInstallationHelper.java @@ -16,7 +16,7 @@ import java.util.Set; import com.fortify.cli.common.util.FcliDataHelper; -import com.fortify.cli.common.util.SemVerHelper; +import com.fortify.cli.common.util.SemVer; import com.fortify.cli.tool.definitions.helper.ToolDefinitionVersionDescriptor; public final class ToolInstallationHelper { @@ -42,10 +42,11 @@ public static final Path getToolsStatePath() { */ public static final boolean isCandidateForUninstall(String toolName, Set versionsToUninstall, ToolDefinitionVersionDescriptor versionDescriptor) { var version = versionDescriptor.getVersion(); + var semver = new SemVer(version); return (versionsToUninstall.contains("all") || containsCandidateForUninstall(versionsToUninstall, version) - || containsCandidateForUninstall(versionsToUninstall, SemVerHelper.getMajor(version).orElse("N/A")) - || containsCandidateForUninstall(versionsToUninstall, SemVerHelper.getMajorMinor(version).orElse("N/A"))) + || containsCandidateForUninstall(versionsToUninstall, semver.getMajor().orElse("N/A")) + || containsCandidateForUninstall(versionsToUninstall, semver.getMajorMinor().orElse("N/A"))) && ToolInstallationDescriptor.load(toolName, versionDescriptor)!=null; } diff --git a/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/_common/helper/ToolUninstaller.java b/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/_common/helper/ToolUninstaller.java index 63e7d851d3..25754e8f12 100644 --- a/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/_common/helper/ToolUninstaller.java +++ b/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/_common/helper/ToolUninstaller.java @@ -23,7 +23,7 @@ import com.fortify.cli.common.json.JsonHelper; import com.fortify.cli.common.util.FcliDataHelper; import com.fortify.cli.common.util.FileUtils; -import com.fortify.cli.common.util.SemVerHelper; +import com.fortify.cli.common.util.SemVer; import com.fortify.cli.tool.definitions.helper.ToolDefinitionVersionDescriptor; import lombok.Data; @@ -44,7 +44,7 @@ public final ToolInstallationOutputDescriptor uninstall(ToolDefinitionVersionDes var action = "UNINSTALLED"; if ( !FileUtils.isDirPathInUse(installPath) ) { FileUtils.deleteRecursive(installPath); - } else if (replacementVersion==null || SemVerHelper.compare(replacementVersion.getVersion(), "2.2.0")<0 ) { + } else if (replacementVersion==null || new SemVer(replacementVersion.getVersion()).compareTo("2.2.0")<0 ) { action = "MANUAL_DELETE_REQUIRED"; } else { action = "PENDING_FCLI_RESTART"; diff --git a/fcli-other/fcli-doc/build.gradle b/fcli-other/fcli-doc/build.gradle index 8b81f58107..dcc7de48be 100644 --- a/fcli-other/fcli-doc/build.gradle +++ b/fcli-other/fcli-doc/build.gradle @@ -29,7 +29,8 @@ task generateActionSchema(type: JavaExec) { dependsOn('build') classpath = sourceSets.main.runtimeClasspath main 'com.fortify.cli.common.action.schema.generator.GenerateActionSchema' - args project.version, actionSchemaOutputDir + // Pass whether this is an (fcli) development release, schema version and output dir + args project.version.startsWith("0."), fcliActionSchemaVersion, actionSchemaOutputDir } task generateManpageAsciiDoc(type: JavaExec) { diff --git a/fcli-other/fcli-doc/src/main/java/com/fortify/cli/common/action/schema/generator/GenerateActionSchema.java b/fcli-other/fcli-doc/src/main/java/com/fortify/cli/common/action/schema/generator/GenerateActionSchema.java index b487cbc6d3..07f13679b1 100644 --- a/fcli-other/fcli-doc/src/main/java/com/fortify/cli/common/action/schema/generator/GenerateActionSchema.java +++ b/fcli-other/fcli-doc/src/main/java/com/fortify/cli/common/action/schema/generator/GenerateActionSchema.java @@ -12,15 +12,17 @@ */ package com.fortify.cli.common.action.schema.generator; -import java.net.HttpURLConnection; +import java.io.FileNotFoundException; +import java.io.IOException; import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; import com.fasterxml.jackson.databind.JsonNode; -import com.fortify.cli.common.action.helper.ActionSchemaVersionHelper; +import com.fortify.cli.common.action.helper.ActionSchemaHelper; import com.fortify.cli.common.action.model.Action; +import com.fortify.cli.common.json.JsonHelper; import com.fortify.cli.common.spring.expression.wrapper.TemplateExpression; import com.github.victools.jsonschema.generator.CustomDefinition; import com.github.victools.jsonschema.generator.Option; @@ -36,30 +38,48 @@ public class GenerateActionSchema { private static final String DEV_VERSION = "dev"; public static void main(String[] args) throws Exception { - if ( args.length!=2 ) { throw new IllegalArgumentException("This command must be run as GenerateActionSchema "); } - var version = args[0]; - var outputPath = Path.of(args[1]); - if ( version.startsWith("0.") ) { version = DEV_VERSION; } - checkSchemaNotExists(version); + if ( args.length!=3 ) { throw new IllegalArgumentException("This command must be run as GenerateActionSchema "); } + var isDevelopmentRelease = args[0]; + var actionSchemaVersion = args[1]; + var outputPath = Path.of(args[2]); + var newSchema = generateSchema(); - Files.createDirectories(outputPath); - var outputFile = outputPath.resolve(String.format("fcli-action-schema-%s.json", version)); - Files.writeString(outputFile, newSchema.toPrettyString(), StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING); - System.out.println("Fortify CLI action schema written to "+outputFile.toString()); + var existingSchema = loadExistingSchema(actionSchemaVersion); + checkSchemaCompatibility(actionSchemaVersion, existingSchema, newSchema); + + // If this is an fcli development release, we output the schema as a development release. + // Note that the same output file name will be used for any branch. + var outputVersion = isDevelopmentRelease.equals("true") ? DEV_VERSION : actionSchemaVersion; + // Only write schema if this is a development release or schema doesn't exist yet. + if ( existingSchema!=null && !DEV_VERSION.equals(outputVersion) ) { + System.out.println("Fortify CLI action schema not being generated as "+outputVersion+" schema already exists"); + } else { + Files.createDirectories(outputPath); + var outputFile = outputPath.resolve(String.format("fcli-action-schema-%s.json", outputVersion)); + Files.writeString(outputFile, newSchema.toPrettyString(), StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING); + System.out.println("Fortify CLI action schema written to "+outputFile.toString()); + } } - private static final void checkSchemaNotExists(String version) throws Exception { - if ( !DEV_VERSION.equals(version) ) { - try { - var url = new URL(ActionSchemaVersionHelper.toURI(version)); - var conn = (HttpURLConnection)url.openConnection(); - conn.setRequestMethod("HEAD"); - if ( conn.getResponseCode()!=404) { - throw new IllegalStateException("Schema version "+version+" has already been published"); - } - } catch ( Exception e ) { - throw e; - } + private static final void checkSchemaCompatibility(String actionSchemaVersion, JsonNode existingSchema, JsonNode newSchema) throws Exception { + if ( existingSchema!=null && !existingSchema.equals(newSchema) ) { + throw new IllegalStateException(String.format(""" + \n\tSchema generated from current source code is different from existing schema + \tversion %s. If this is incorrect (action model hasn't changed), please update + \tthe schema compatibility check in .../fcli-doc/src/.../GenerateActionSchema.java. + \tIf the schema has indeed changed, please update the schema version number in + \tgradle.properties in the root fcli project. + """, actionSchemaVersion)); + } + } + + private static final JsonNode loadExistingSchema(String actionSchemaVersion) throws IOException { + try { + return JsonHelper.getObjectMapper().readTree(new URL(ActionSchemaHelper.toURI(actionSchemaVersion))); + } catch ( FileNotFoundException fnfe ) { + return null; // Schema doesn't exist yet + } catch ( IOException e ) { + throw e; } } diff --git a/gradle.properties b/gradle.properties index 698be5ab7b..9bdd4209b2 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,7 +17,6 @@ fcliUtilRef=:fcli-core:fcli-util fcliBomRef=:fcli-other:fcli-bom fcliFunctionalTestRef=:fcli-other:fcli-functional-test fcliAutoCompleteRef=:fcli-other:fcli-autocomplete -fcliActionSchemaRef=:fcli-other:fcli-action-schema fcliDocRef=:fcli-other:fcli-doc # TODO Remove once patch is available for # https://github.com/formkiq/graalvm-annotations-processor/issues/9 @@ -38,3 +37,17 @@ fcliRootCommandsClassName=com.fortify.cli.app._main.cli.cmd.FCLIRootCommands // Define the main class name for running fcli. // FortifyCLITest checks that this property contains a valid class name. fcliMainClassName=com.fortify.cli.app.FortifyCLI + +// Define fcli action schema version. This must be manually maintained, and must +// be updated whenever the action schema model is changed: +// - Increase patch version for non-structural changes, like description updates. +// - Increase minor version for backward-compatible structural changes, like +// adding new (optional) step types or adding new optional properties to existing +// types. +// - Increase major version for non-backward-compatible changes, like adding new +// required properties, or changing the meaning/value type of an existing property. +// To allow for proper detection of whether a given fcli version is compatible with a +// given schema version, it is very important to maintain this correctly. At all cost, +// we should avoid for example updating only patch version if there are any structural +// changes. +fcliActionSchemaVersion=1.0.0