Azure / typespec-azure

About TypeSpec Azure Libraries
https://azure.github.io/typespec-azure/
MIT License
13 stars 39 forks source link

Improve TypeSpec versioning story #1621

Open allenjzhang opened 2 weeks ago

allenjzhang commented 2 weeks ago

Versioning feels like a detective's case map

timotheeguerin commented 6 hours ago

Versioning rework - user side

Problems with versioning today:

Other problems that this proposal doesn't cover:

Azure versioning vs other versioning

Azure services use api versioning in a specific way that a lot of other services do not. The versioning library needs to be able to accommodate both. We can see versioning of a service as the following:

  1. [Azure way] every version maps to a different api version which might or might not have breaking changes. Api version should be immutable(Taking a snapshot at a specific api version should represent that api accurately at any point in time where it exists)
  2. Additive changes are done within the same api version(e.g. using a v1/v2 sub path). In those apis a client would call the same api and get the new data but as it is additive it wouldn't break. The versioning decorators would be mostly for documentation purposes.

Preview vs Stable

For this we would want 2 things potentially:

  1. A way to differentiate a preview from a stable API.
  2. A way to branch preview features.

For 1. we should be able to use a new @prerelease or @preview decorator on a version enum member to mark it as a preview version. Additionally we could auto detect a preview version if the service use some semver

enum Versions {
  `1.0`, // stable
  @prerelease
  `2.0-preview.1`, // Explicitly marked as a preview
  `2.0-beta.1`, // Implicitly inferred as a semver prerelease.
  `2.0` // stable

}

For 2. it is a bit more tricky, this introduce a complex branching system that would be quite hard to reason about. What preview features made it to the next stable version? One potential solution would be that any versioning annotation about a preview version do not apply towards the next stable version. This would mean you need to be explicit about the branching of a preview feature.

enum Versions {
  `1.0`, // stable
  @preview(Versions.`1.0`)
  `2.0-preview.feat1`,

  @preview(Versions.`1.0`)
  `2.0-preview.feat2`,

  `2.0` // stable
}

model Pet {
  name: string;

  @added(Versions.`2.0-preview.feat1`)
  age: number;

  @added(Versions.`2.0-preview.feat2`)
  @added(Versions.`2.0`) // Here we say in v2 we actually picked this property
  dob: plainDate;
}

This approach might sound intresting but it also brings quite some complexity to the table and using versioning for a flag based feature branching might not be the most appropriate.

Another approach could be to use copy of a spec for preview versioning and merging back into the stable folder when moving out of preview. This would keep the main branch clean of preview versioning and would allow to have a clear view of what is in the next stable version without needing immediate cleanup(as the next section describe).

Additional tooling

Depending on how preview version evolve they can result in a lot of unnecessary noise in the spec when the next stable is reached. We should provide a way to patch a spec and remove any preview version from a spec. The tool would automatically updated versioning decorators accordingly. In the case of 2. we would also most likely want a tool to apply the annotations to the next stable version.

Adding a new version

In the ideal scenario where you are just doing additive change you should just be able to use the @added decorator. However depending on the size of the new version it might become tedious to apply @added to every new property, model, etc. One suggestion was to allow using versioning decorators on namespace to apply to all children.

Approach 1: Versioning of namespace


namespace MyService {
  model Cat {}

  @added(Versions.v2)
  namespace V2 {
    model Dog {}
  }
}

I think the main problem with this is you are structuring your service according to versioning which could cause issues in emitters that use the namespace as a way to structure the output. Additionally it is quite unlikely that the version change are contained within a single namespace and that would still require a lot of @added decorators in other places.

Approach 2: Versioning inference

Another alternative that was suggested was to infer versioning attributes automatically

model Pet {
  adddress: Address;
}

@added(Versions.v2)
model Address {}

in the example above we could argue that address: Address should be considered as a v2 change automatically. I think this might also sound helpful until you look at Pet and don't understand why address is not in v1 unless you start to see that this model was added in v2. That in more complex scenarios would make it much harder to differentiate user errors from versioning done on purpose.

Approach 3: Provide decoration of abastract block

This is a similar approach to 1. but would mean adding a new language feature that would allow decorating an block code expression


namespace MyService {
  model Cat {}

  @added(Versions.v2)
  {
    model Dog {}
  }
}

The way this would work is applying the decorators to each elements in the code block. We'd have to decide what happens if the decorator doesn't apply to some of the elements inside. Does it ignore or it errors out.