deployment-tools icon indicating copy to clipboard operation
deployment-tools copied to clipboard

Productize DOM APIs for release.json

Open terrajobst opened this issue 5 years ago • 3 comments

@joeloff is working on this. The APIs can be reviewed here (internal only link, sorry). The APIs are here:

API Propsal

namespace Microsoft.Deployment.DotNet.Releases {
    public class AspNetCoreReleaseComponent : ReleaseComponent {
        public IReadOnlyCollection<string> AspNetCoreModuleVersions { get; }
        public string VisualStudioVersion { get; }
    }
    public class Cve {
        public Cve();
        public string Id { get; }
        public Uri Url { get; }
        public override bool Equals(object obj);
        public override int GetHashCode();
    }
    public class Product {
        public Product();
        public DateTime? EndOfLifeDate { get; }
        public bool IsSecurityUpdate { get; }
        public DateTime? LatestReleaseDate { get; }
        public ReleaseVersion LatestReleaseVersion { get; }
        public ReleaseVersion LatestRuntimeVersion { get; }
        public ReleaseVersion LatestSdkVersion { get; }
        public string ProductName { get; }
        public string ProductVersion { get; }
        public Uri ReleasesJson { get; }
        public SupportPhase SupportPhase { get; }
        public static Task<ReadOnlyCollection<ProductRelease>> GetReleasesAsync(Uri releasesJsonUrl);
        public Task<ReadOnlyCollection<ProductRelease>> GetReleasesAsync();
        public Task<ReadOnlyCollection<ProductRelease>> GetReleasesAsync(string releasesIndexJsonPath, bool downloadLatest);
        public bool IsOutOfSupport();
    }
    public sealed class ProductCollection : ReadOnlyCollection<Product> {
        public static readonly Uri ReleaseIndexDefaultUrl;
        public static Task<ProductCollection> CreateAsync();
        public static Task<ProductCollection> CreateAsync(string releasesIndexJsonPath, bool downloadLatest);
        public static Task<ProductCollection> CreateAsync(Uri releasesIndexUrl);
        public IEnumerable<SupportPhase> GetSupportPhases();
    }
    public class ProductRelease {
        public AspNetCoreReleaseComponent AspNetCoreRuntime { get; }
        public IReadOnlyCollection<ReleaseComponent> Components { get; }
        public IReadOnlyCollection<Cve> Cves { get; }
        public IReadOnlyCollection<ReleaseFile> Files { get; }
        public bool IsPreview { get; }
        public bool IsSecurityUpdate { get; }
        public DateTime ReleaseDate { get; }
        public Uri ReleaseNotes { get; }
        public RuntimeReleaseComponent Runtime { get; }
        public IEnumerable<ReleaseComponent> Runtimes { get; }
        public IReadOnlyCollection<SdkReleaseComponent> Sdks { get; }
        public ReleaseVersion Version { get; }
        public WindowsDesktopReleaseComponent WindowsDesktopRuntime { get; }
    }
    public abstract class ReleaseComponent {
        public ReleaseVersion DisplayVersion { get; }
        public IReadOnlyCollection<ReleaseFile> Files { get; }
        public string Name { get; protected set; }
        public ProductRelease Release { get; }
        public ReleaseVersion Version { get; }
    }
    public class ReleaseFile {
        public ReleaseFile();
        public string FileName { get; }
        public string Hash { get; }
        public string Name { get; }
        public string Rid { get; }
        public Uri Url { get; }
        public Task DownloadAsync(string fileName);
        public Task DownloadAsync(string fileName, bool verifyHash);
        public override bool Equals(object obj);
        public override int GetHashCode();
    }
    public class ReleaseVersion : IComparable, IComparable<ReleaseVersion>, IEquatable<ReleaseVersion>, ICloneable {
        public ReleaseVersion(string version);
        public ReleaseVersion();
        public static readonly string Version2Pattern;
        public string BuildMetadata { get; set; }
        public int Major { get; set; }
        public int Minor { get; set; }
        public int Patch { get; set; }
        public string Prerelease { get; set; }
        public int SdkFeatureBand { get; }
        public int SdkPatchLevel { get; }
        public static int Compare(ReleaseVersion a, ReleaseVersion b);
        public static bool Equals(ReleaseVersion a, ReleaseVersion b);
        public static bool operator ==(ReleaseVersion a, ReleaseVersion b);
        public static bool operator >(ReleaseVersion a, ReleaseVersion b);
        public static bool operator >=(ReleaseVersion a, ReleaseVersion b);
        public static bool operator !=(ReleaseVersion a, ReleaseVersion b);
        public static bool operator <(ReleaseVersion a, ReleaseVersion b);
        public static bool operator <=(ReleaseVersion a, ReleaseVersion b);
        public object Clone();
        public int ComparePrecedence(ReleaseVersion value);
        public int CompareTo(object value);
        public int CompareTo(ReleaseVersion value);
        public bool Equals(ReleaseVersion obj);
        public bool PrecedenceEquals(ReleaseVersion value);
        public string ToString(int fieldCount);
        public override bool Equals(object obj);
        public override int GetHashCode();
        public override string ToString();
    }
    public class ReleaseVersionConverter : JsonConverter<ReleaseVersion> {
        public ReleaseVersionConverter();
        public override ReleaseVersion ReadJson(JsonReader reader, Type objectType, ReleaseVersion existingValue, bool hasExistingValue, JsonSerializer serializer);
        public override void WriteJson(JsonWriter writer, ReleaseVersion value, JsonSerializer serializer);
    }
    public class RuntimeReleaseComponent : ReleaseComponent {
        public string VisualStudioMacVersion { get; }
        public string VisualStudioVersion { get; }
    }
    public class SdkReleaseComponent : ReleaseComponent {
        public string CSharpVersion { get; }
        public string FSharpVersion { get; }
        public ReleaseVersion RuntimeVersion { get; }
        public string VisualBasicVersion { get; }
        public string VisualStudioMacSupport { get; }
        public string VisualStudioMacVersion { get; }
        public string VisualStudioSupport { get; }
        public string VisualStudioVersion { get; }
    }
    public class SupportPhaseConverter : StringEnumConverter {
        public SupportPhaseConverter();
        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer);
    }
    public class WindowsDesktopReleaseComponent : ReleaseComponent {
    }
    public enum SupportPhase {
        Unknown = 0,
        [EnumMember]
        EndOfLife = 1,
        Maintenance = 2,
        [EnumMember]
        LongTermSupport = 3,
        Preview = 4,
        RC = 5,
    }
}

terrajobst avatar Sep 28 '20 21:09 terrajobst

Video

  • Provides a public API for the release.json
  • Distributed as a NuGet package
  • Depends on Newtonsoft.Json
  • Is Microsoft.Deployment.Releases the right namespace? Should we fit into the other namespaces used by the SDK tooling, which AFAIK is in in Microsoft.DotNet.
  • Currently Windows-only (because it targets .NET Framework) but it can be fully cross-platform
  • General notes:
    • Instead of exposing IEnumerable<T> use collection types Collection<T> and ReadOnlyCollection<T> because they avoid leaking the underlying instance that holds the data. It also allows you to derive from to add custom indexers/methods.
    • Using DateTime over DateTimeOffset for data that has no time component is correct
  • ReleaseIndex
    • It's a subset of ReleaseChannel. It would be better if these types would be merged and the additional data is lazily loaded. For example Product would represent the data that is in the root JSON file and ProductDetails would contain the entirety of the product that comes from another JSON file. They key is to let the user initiate each separate IO request. Should also return data async.
  • ProductVersion
    • ReleaseVersion
  • ReleaseChannel
    • ReleaseChannel should be named Product
    • SupportPhase should be an enum
    • It's weird to have LatestRelease and GetLatestRelease(). Either get rid of the method and make the property return Release or remove the properties.
    • Having a nullable LatestReleaseDate is odd
    • Shouldn't have public constructor
  • Release
    • Runtimes should be Components
    • Having both Sdk and Sdks is odd. We should only expose Sdks because that's well defined
    • AspNetCoreRuntime should be AspNetCoreComponent
    • WindowsDesktopRuntime should be WindowsDesktopComponent
  • IRelease
    • It's odd to have a type called Release but not implementing IRelease
    • Also, we should try to avoid interfaces and use abstract base types
    • Should be named ReleaseComponent and an abstract class
  • SdkRelease
    • Should be named SdkReleaseComponent
  • RuntimeRelease
    • Should be named RuntimeReleaseComponent
  • AspNetCoreRuntimeRelease
    • Should be named AspNetCoreReleaseComponent
  • WindowsDesktopRelease
    • Should be named WindowsDesktopReleaseComponent
namespace Microsoft.Deployment.Releases {
    public interface IRelease {
        IEnumerable<ReleaseFile> Files { get; set; }
        string Name { get; }
        ReleaseFlags ReleaseKind { get; }
        ProductVersion Version { get; set; }
    }
    public class AspNetCoreRuntimeRelease : IRelease {
        public AspNetCoreRuntimeRelease();
        public string[] AspNetCoreModuleVersions { get; set; }
        public string DisplayVersion { get; set; }
        public IEnumerable<ReleaseFile> Files { get; set; }
        public string Name { get; }
        public ProductVersion Version { get; set; }
        public string VisualStudioVersion { get; set; }
    }
    public class Cve {
        public Cve();
        public string Id { get; set; }
        public Uri Url { get; set; }
        public override bool Equals(object obj);
        public override int GetHashCode();
    }
    public class ProductVersion : IComparable, IComparable<ProductVersion>, IEquatable<ProductVersion>, ICloneable {
        public ProductVersion(string version);
        public ProductVersion();
        public static readonly string Version2Pattern;
        public string BuildMetadata { get; set; }
        public int Major { get; set; }
        public int Minor { get; set; }
        public int Patch { get; set; }
        public string Prerelease { get; set; }
        public int SdkFeatureBand { get; }
        public int SdkPatchLevel { get; }
        public static int Compare(ProductVersion a, ProductVersion b);
        public static bool Equals(ProductVersion a, ProductVersion b);
        public static bool operator ==(ProductVersion v1, ProductVersion v2);
        public static bool operator >(ProductVersion v1, ProductVersion v2);
        public static bool operator >=(ProductVersion v1, ProductVersion v2);
        public static bool operator !=(ProductVersion v1, ProductVersion v2);
        public static bool operator <(ProductVersion v1, ProductVersion v2);
        public static bool operator <=(ProductVersion v1, ProductVersion v2);
        public object Clone();
        public int ComparePrecedence(ProductVersion value);
        public int CompareTo(object value);
        public int CompareTo(ProductVersion value);
        public bool Equals(ProductVersion obj);
        public bool PrecedenceEquals(ProductVersion value);
        public string ToString(int fieldCount);
        public override bool Equals(object obj);
        public override int GetHashCode();
        public override string ToString();
    }
    public class ProductVersionConverter : JsonConverter<ProductVersion> {
        public ProductVersionConverter();
        public override ProductVersion ReadJson(JsonReader reader, Type objectType, ProductVersion existingValue, bool hasExistingValue, JsonSerializer serializer);
        public override void WriteJson(JsonWriter writer, ProductVersion value, JsonSerializer serializer);
    }
    public class Release {
        public Release();
        public AspNetCoreRuntimeRelease AspNetCoreRuntime { get; set; }
        public IEnumerable<Cve> Cves { get; set; }
        public IEnumerable<ReleaseFile> Files { get; }
        public bool IsPreview { get; }
        public bool IsSecurityUpdate { get; set; }
        public DateTime ReleaseDate { get; set; }
        public Uri ReleaseNotes { get; set; }
        public ProductVersion ReleaseVersion { get; set; }
        public RuntimeRelease Runtime { get; set; }
        public IEnumerable<IRelease> Runtimes { get; }
        public SdkRelease Sdk { get; set; }
        public IEnumerable<SdkRelease> Sdks { get; set; }
        public WindowsDesktopRelease WindowsDesktop { get; set; }
    }
    public class ReleaseChannel {
        public ReleaseChannel();
        public string ChannelVersion { get; set; }
        public DateTime? EolDate { get; set; }
        public bool IsOutOfSupport { get; }
        public ProductVersion LatestRelease { get; set; }
        public DateTime? LatestReleaseDate { get; set; }
        public ProductVersion LatestRuntime { get; set; }
        public ProductVersion LatestSdk { get; set; }
        public Uri LifeCyclePolicyUrl { get; set; }
        public IEnumerable<Release> Releases { get; set; }
        public SupportPhase SupportPhase { get; set; }
        public Release GetLatestRelease();
        public Release GetLatestRelease(bool isSecurityUpdate);
    }
    public class ReleaseFile {
        public ReleaseFile();
        public string FileName { get; }
        public string Hash { get; }
        public string Name { get; }
        public string Rid { get; }
        public Uri Url { get; }
        public void Download(string fileName);
        public override bool Equals(object obj);
        public override int GetHashCode();
    }
    public class ReleaseIndex {
        public ReleaseIndex();
        public string ChannelVersion { get; set; }
        public DateTime? EolDate { get; set; }
        public bool IsOutOfSupport { get; }
        public bool IsSecurity { get; set; }
        public ProductVersion LatestRelease { get; set; }
        public DateTime? LatestReleaseDate { get; set; }
        public ProductVersion LatestRuntime { get; set; }
        public ProductVersion LatestSdk { get; set; }
        public string Product { get; set; }
        public string ReleasesJson { get; set; }
        public SupportPhase SupportPhase { get; set; }
    }
    public class Releases {
        public Releases();
        public static readonly Uri ReleasesIndexJsonUri;
        public int ChannelCount { get; }
        public IEnumerable<ReleaseChannel> Channels { get; }
        public IEnumerable<string> ChannelVersions { get; }
        public IEnumerable<ReleaseIndex> Index { get; set; }
        public ReleaseChannel this[string channelVersion] { get; }
        public static Releases CreateFromDefaultUrl();
        public static Releases CreateFromFile(string releasesIndexPath);
        public static Releases CreateFromFile(string releasesIndexPath, bool useLatest);
        public static Releases CreateFromUrl(Uri uri);
        public bool ContainsChannel(string channelVersion);
        public Release GetRelease(ProductVersion releaseVersion);
        public IEnumerable<Release> GetReleases(string cveId);
    }
    public class ReleasesHelpers {
        public ReleasesHelpers();
        public static T Create<T>(Uri jsonUrl);
        public static T CreateFromFile<T>(string path);
    }
    public class RuntimeRelease : IRelease {
        public RuntimeRelease();
        public ProductVersion DisplayVersion { get; set; }
        public IEnumerable<ReleaseFile> Files { get; set; }
        public string Name { get; }
        public ProductVersion Version { get; set; }
        public string VisualStudioMacVersion { get; set; }
        public string VisualStudioVersion { get; set; }
    }
    public class SdkRelease : IRelease {
        public SdkRelease();
        public string CSharpVersion { get; set; }
        public ProductVersion DisplayVersion { get; set; }
        public IEnumerable<ReleaseFile> Files { get; set; }
        public string FSharpVersion { get; set; }
        public string Name { get; }
        public ProductVersion RuntimeVersion { get; set; }
        public ProductVersion Version { get; set; }
        public string VisualStudioMacSupport { get; set; }
        public string VisualStudioMacVersion { get; set; }
        public string VisualStudioSupport { get; set; }
        public string VisualStudioVersion { get; set; }
    }
    public class WindowsDesktopRelease : IRelease {
        public WindowsDesktopRelease();
        public string DisplayVersion { get; set; }
        public IEnumerable<ReleaseFile> Files { get; set; }
        public string Name { get; }
        public ProductVersion Version { get; set; }
    }
    [Flags]
    public enum ReleaseFlags {
        None = 0,
        Sdk = 1,
        Runtime = 2,
        AspNetCoreRuntime = 4,
        WindowsDesktopRuntime = 8,
    }
    public enum SupportPhase {
        Undefined = 0,
        [EnumMember]
        EOL = 1,
        [EnumMember]
        LTS = 2,
        [EnumMember]
        Maintenance = 3,
        [EnumMember]
        Preview = 4,
    }
}

terrajobst avatar Sep 29 '20 19:09 terrajobst

With regards to the namespace question...I see this as something that the SDK might use, but most applications would be outside the SDK space, e.g. a global tool to query released products, installation managers, etc.

Would Microsoft.Deployment.DotNet.Products perhaps capture the intent better?

joeloff avatar Sep 30 '20 20:09 joeloff

Video

  • Pick a strategy: either expose IReadOnlyXxx or use Collection/ReadOnlyCollection<T> types but don't do both
  • Some types of the object model have (public) constructors while others don't. Pick one model.
  • Is it possible that consumers of this API can observe inconsistencies when files are updated independently? Should this be done via a versioning scheme or simply by serving the files from a GitHub repo (so long we make sure that commits are consistent).
  • ProductCollection
    • ReleaseIndexDefaultUrl should be a get-only property
    • CreateAsync is odd. How about GetAsync()?
    • The parameters shouldn't use name Url or Json.
    • Don't overload between file paths and URLs (because we often also add string based overloads for URI based APIs)
    • Add a string-based overload for URI
    • Add GetFromFile for the file-based one
  • Product
    • IsSecurityUpdate. Should at least be renamed LatestReleaseIsSecurityUpdate but we should consider removing it because the SDK has feature trains 5.0.100 and 5.0.200 that are for different VS releases. In which case only 200 is latest, which produce confusing/misleading results. It also seems the property is prone to errors for folks that want to check whether they are missing a security update (because this property is only true when the very latest release is a security update).
    • The static GetReleasesAsync(Uri releasesJsonUrl) method should move to ProductRelease
  • SupportPhase
    • We should probably not surface serialization attributes
    • What does Maintenance mean?
    • Current is missing
  • ProductRelease
    • Whatever policy you have on the Product, such as whether something is go-live SupportPhase it seems the releases should be able to answer any questions as well, because releases are logically a point in time for a product. For example, we expose IsPreview but not IsGoLive.
    • Considering a reference back to Product
  • ReleaseComponent
    • DisplayVersion should be a string
    • Runtimes should be named AllRuntimes and return IReadOnlyCollection<T>
  • ReleaseVersion
    • This type is mutable, which we probably don't want. We should also remove Clone() and ICloneable
    • You have ComparePrecedence and PrecedenceEquals. It seems more consistent to use PrecedenceCompareTo and PrecedenceEquals
  • ReleaseFile
    • We should make sure that hash validation is the default.
    • When the validation fails we should delete the file
    • We should negate the parameter to skipHashValidation
    • Should we consider dropping the option of skipping entirely?
    • Consider using System.IO.InvalidDataException
    • The fileName parameter is a bit ambiguous because the instance also has one. Maybe path or destinationPath?
    • Should probably implement IEquatable<>
  • ReleaseVersionConverter and SupportPhaseConverter
    • Should probably be internal
namespace Microsoft.Deployment.DotNet.Releases {
    public class AspNetCoreReleaseComponent : ReleaseComponent {
        public IReadOnlyCollection<string> AspNetCoreModuleVersions { get; }
        public string VisualStudioVersion { get; }
    }
    public class Cve {
        public Cve();
        public string Id { get; }
        public Uri Url { get; }
        public override bool Equals(object obj);
        public override int GetHashCode();
    }
    public class Product {
        public Product();
        public DateTime? EndOfLifeDate { get; }
        public bool IsSecurityUpdate { get; }
        public DateTime? LatestReleaseDate { get; }
        public ReleaseVersion LatestReleaseVersion { get; }
        public ReleaseVersion LatestRuntimeVersion { get; }
        public ReleaseVersion LatestSdkVersion { get; }
        public string ProductName { get; }
        public string ProductVersion { get; }
        public Uri ReleasesJson { get; }
        public SupportPhase SupportPhase { get; }
        public static Task<ReadOnlyCollection<ProductRelease>> GetReleasesAsync(Uri releasesJsonUrl);
        public Task<ReadOnlyCollection<ProductRelease>> GetReleasesAsync();
        public Task<ReadOnlyCollection<ProductRelease>> GetReleasesAsync(string releasesIndexJsonPath, bool downloadLatest);
        public bool IsOutOfSupport();
    }
    public sealed class ProductCollection : ReadOnlyCollection<Product> {
        public static readonly Uri ReleaseIndexDefaultUrl;
        public static Task<ProductCollection> CreateAsync();
        public static Task<ProductCollection> CreateAsync(string releasesIndexJsonPath, bool downloadLatest);
        public static Task<ProductCollection> CreateAsync(Uri releasesIndexUrl);
        public IEnumerable<SupportPhase> GetSupportPhases();
    }
    public class ProductRelease {
        public AspNetCoreReleaseComponent AspNetCoreRuntime { get; }
        public IReadOnlyCollection<ReleaseComponent> Components { get; }
        public IReadOnlyCollection<Cve> Cves { get; }
        public IReadOnlyCollection<ReleaseFile> Files { get; }
        public bool IsPreview { get; }
        public bool IsSecurityUpdate { get; }
        public DateTime ReleaseDate { get; }
        public Uri ReleaseNotes { get; }
        public RuntimeReleaseComponent Runtime { get; }
        public IEnumerable<ReleaseComponent> Runtimes { get; }
        public IReadOnlyCollection<SdkReleaseComponent> Sdks { get; }
        public ReleaseVersion Version { get; }
        public WindowsDesktopReleaseComponent WindowsDesktopRuntime { get; }
    }
    public abstract class ReleaseComponent {
        public ReleaseVersion DisplayVersion { get; }
        public IReadOnlyCollection<ReleaseFile> Files { get; }
        public string Name { get; protected set; }
        public ProductRelease Release { get; }
        public ReleaseVersion Version { get; }
    }
    public class ReleaseFile {
        public ReleaseFile();
        public string FileName { get; }
        public string Hash { get; }
        public string Name { get; }
        public string Rid { get; }
        public Uri Url { get; }
        public Task DownloadAsync(string fileName);
        public Task DownloadAsync(string fileName, bool verifyHash);
        public override bool Equals(object obj);
        public override int GetHashCode();
    }
    public class ReleaseVersion : IComparable, IComparable<ReleaseVersion>, IEquatable<ReleaseVersion>, ICloneable {
        public ReleaseVersion(string version);
        public ReleaseVersion();
        public static readonly string Version2Pattern;
        public string BuildMetadata { get; set; }
        public int Major { get; set; }
        public int Minor { get; set; }
        public int Patch { get; set; }
        public string Prerelease { get; set; }
        public int SdkFeatureBand { get; }
        public int SdkPatchLevel { get; }
        public static int Compare(ReleaseVersion a, ReleaseVersion b);
        public static bool Equals(ReleaseVersion a, ReleaseVersion b);
        public static bool operator ==(ReleaseVersion a, ReleaseVersion b);
        public static bool operator >(ReleaseVersion a, ReleaseVersion b);
        public static bool operator >=(ReleaseVersion a, ReleaseVersion b);
        public static bool operator !=(ReleaseVersion a, ReleaseVersion b);
        public static bool operator <(ReleaseVersion a, ReleaseVersion b);
        public static bool operator <=(ReleaseVersion a, ReleaseVersion b);
        public object Clone();
        public int ComparePrecedence(ReleaseVersion value);
        public int CompareTo(object value);
        public int CompareTo(ReleaseVersion value);
        public bool Equals(ReleaseVersion obj);
        public bool PrecedenceEquals(ReleaseVersion value);
        public string ToString(int fieldCount);
        public override bool Equals(object obj);
        public override int GetHashCode();
        public override string ToString();
    }
    public class ReleaseVersionConverter : JsonConverter<ReleaseVersion> {
        public ReleaseVersionConverter();
        public override ReleaseVersion ReadJson(JsonReader reader, Type objectType, ReleaseVersion existingValue, bool hasExistingValue, JsonSerializer serializer);
        public override void WriteJson(JsonWriter writer, ReleaseVersion value, JsonSerializer serializer);
    }
    public class RuntimeReleaseComponent : ReleaseComponent {
        public string VisualStudioMacVersion { get; }
        public string VisualStudioVersion { get; }
    }
    public class SdkReleaseComponent : ReleaseComponent {
        public string CSharpVersion { get; }
        public string FSharpVersion { get; }
        public ReleaseVersion RuntimeVersion { get; }
        public string VisualBasicVersion { get; }
        public string VisualStudioMacSupport { get; }
        public string VisualStudioMacVersion { get; }
        public string VisualStudioSupport { get; }
        public string VisualStudioVersion { get; }
    }
    public class SupportPhaseConverter : StringEnumConverter {
        public SupportPhaseConverter();
        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer);
    }
    public class WindowsDesktopReleaseComponent : ReleaseComponent {
    }
    public enum SupportPhase {
        Unknown = 0,
        [EnumMember]
        EndOfLife = 1,
        Maintenance = 2,
        [EnumMember]
        LongTermSupport = 3,
        Preview = 4,
        RC = 5,
    }
}

terrajobst avatar Oct 13 '20 19:10 terrajobst