From b9fcaf11d6a0f92434b75e1306e66240305bb6f0 Mon Sep 17 00:00:00 2001 From: Eivind Grimstad Date: Fri, 7 Aug 2026 17:00:19 +0200 Subject: [PATCH 1/3] feat(ETU-75494): Remove paragraph about breaking changes Will be described in new paragraph instead --- .spectral.yml | 6 ------ guidelines.md | 7 +------ 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/.spectral.yml b/.spectral.yml index cf5b7e7..59391a8 100644 --- a/.spectral.yml +++ b/.spectral.yml @@ -363,12 +363,6 @@ rules: # 3.2 Versioning # ------------------------------------------------------------------------- - - # ------------------------------------------------------------------------- - # 3.3 Backward Compatibility - Not directly lintable - # ------------------------------------------------------------------------- - - # ============================================================================= # 4. Communication Standards # ============================================================================= diff --git a/guidelines.md b/guidelines.md index 126aa5f..0f9f3de 100644 --- a/guidelines.md +++ b/guidelines.md @@ -292,12 +292,7 @@ published, the current year and month is used, and MICRO is set to "00". If a ve If the published spec is equal to the current spec, no new version is created. Because versioning is done automatically, the value in `info.version` is ignored, but must be set in order for linting to pass, therefore a placeholder value like `1.0.0` may be used. -### 3.3 Backward Compatibility -- :eyes: You **MUST** not remove or modify existing fields or endpoints -- :eyes: You **MUST** introduce new versions for changes that break previous contracts -- :eyes: You **MUST** clearly document which features are deprecated and provide guidance for migration - -### 3.4 Tags Naming +### 3.3 Tags Naming :eyes: Tags should be used as a logical grouping that reflects functional domains, use cases or data types, not internal architecture. - Bad example: `internal`, `partner`, `production`, `route-service-v2`, `misc`, `other` - Good example, when using tags for functional domains: `Journey Planning`, `Realtime Departures` From 91e45d1f77ec656d831ef13c8525e01b295ac01e Mon Sep 17 00:00:00 2001 From: Eivind Grimstad Date: Fri, 7 Aug 2026 16:37:30 +0200 Subject: [PATCH 2/3] feat(ETU-75494): Start work on guidelines about API lifecycle --- guidelines.md | 48 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/guidelines.md b/guidelines.md index 0f9f3de..39eed63 100644 --- a/guidelines.md +++ b/guidelines.md @@ -262,6 +262,48 @@ Here, only 1 specification will be shown on the Developer Portal, which is a com +### 2.5 Lifecycle +All APIs, and endpoints within an API, follow a lifecycle with different stages that have different properties. + +1. Design and development: The shape of the API is being figured out. Changes happen often. +2. Testing: Feedback is gathered. Changes happen, and more design and development may be necessary. +3. Stabilization and production use: The API is relied upon by consumers. Changes may happen, as long as they are not breaking. +4. Deprecation: The API is marked as deprecated and consumers are encouraged to move to a new version. Only critical changes will happen. +5. Sunsetting: The API is removed. + +The following sections describe a standardized way to declare an API's lifecycle status in its specification. Following this standard has several benefits: +- It is easier for consumers to understand what they can expect from an API in terms of stability. +- It is easier to enforce Enturs common deprecation rules. + +#### 2.5.1 Design and development +- :ballot_box_with_check: The field `x-stability-level` **MUST** have the value `draft` +- :ballot_box_with_check: The field `deprecated` **MUST** be empty or have the value `false` +- :ballot_box_with_check: The field `x-sunset` **MUST** be empty + +#### 2.5.2 Testing and feedback +- :ballot_box_with_check: The field `x-stability-level` **MUST** have the value `beta` +- :ballot_box_with_check: The field `deprecated` **MUST** be empty or have the value `false` +- :ballot_box_with_check: The field `x-sunset` **MUST** be empty + +#### 2.5.3 Production use +- :ballot_box_with_check: The field `x-stability-level` **MUST** have the value `stable` or be empty +- :ballot_box_with_check: The field `deprecated` **MUST** be empty or have the value `false` +- :ballot_box_with_check: The field `x-sunset` **MUST** be empty +- :eyes: You **MUST NOT** make [breaking changes](#I-breaking-changes) + +#### 2.5.4 Deprecation +- :ballot_box_with_check: The field `deprecated` **MUST** have the value `true` +- :ballot_box_with_check: The field `x-sunset` **MUST** filled with a date that follows Enturs rules for deprecation. +- :ballot_box_with_check: The field `x-stability-level` **MUST** be empty + +#### 2.5.5 Sunsetting +- :ballot_box_with_check: When the date in `x-sunset` has passed, the endpoint or API **SHOULD** be removed. Otherwise, the value should be changed. + +#### 2.5.6 *Notes* +- When an entire API has an `x-stability-level` other than `stable`, individual endpoints may not declare an `x-stability-level` +- When an entire API has `x-deprecated: true`, individual endpoints may not declare `x-stability-level` or `deprecated`. +- `deprecated: true` must not be set together with any other value for `x-stability-level` than `stable`. + ## 3. Naming & Structure Conventions @@ -551,10 +593,14 @@ There may be situations where a pure REST architecture is not the best solution. - **WebSocket**: For real-time bidirectional communication - **gRPC**: For high-performance, strongly-typed services - ## FAQ *Must existing APIs conform the guidelines?* - Non-breaking changes (like adding example values) **SHOULD** be updated to be compliant with the guidelines. - Breaking changes **MAY** be added in a new version of the API. - New APIs **MUST** follow the guidelines. + +## Appendix + +## I. Breaking changes +Here we should write about what we mean with breaking changes. From 8d6c36569a33795de9ba4a30d17b64f365dae97d Mon Sep 17 00:00:00 2001 From: Eivind Grimstad Date: Mon, 10 Aug 2026 17:11:00 +0200 Subject: [PATCH 3/3] feat(ETU-75494): Add lint rules and improve text --- .spectral.yml | 84 +++++++++++++++++++++++++++++++++++++++++++++++ functions/date.js | 14 ++++++++ guidelines.md | 63 ++++++++++++++++++++++------------- 3 files changed, 139 insertions(+), 22 deletions(-) create mode 100644 functions/date.js diff --git a/.spectral.yml b/.spectral.yml index 59391a8..81ad34b 100644 --- a/.spectral.yml +++ b/.spectral.yml @@ -6,6 +6,7 @@ extends: [spectral:oas] functions: + - date - requireExampleOrRef - requireRequestBodyDescription @@ -282,6 +283,89 @@ rules: functionOptions: match: ^[a-z0-9]+(-[a-z0-9]+)*$ + # ------------------------------------------------------------------------- + # 2.5 Lifecycle + # ------------------------------------------------------------------------- + + ## On API level + entur-stability-level-api: + documentationUrl: "https://github.com/entur/api-guidelines/blob/main/guidelines.md#25-lifecycle" + severity: error + given: $.info.x-stability-level + then: + function: enumeration + functionOptions: + values: [draft, beta, stable] + + entur-deprecation-api: + documentationUrl: "https://github.com/entur/api-guidelines/blob/main/guidelines.md#25-lifecycle" + severity: error + given: $.info.x-deprecated + then: + function: schema + functionOptions: + schema: + type: boolean + + entur-sunset-format-api: + documentationUrl: "https://github.com/entur/api-guidelines/blob/main/guidelines.md#25-lifecycle" + severity: error + given: $[?(@property === "info" && @["x-deprecated"] === true)] + then: + field: x-sunset + function: date + + entur-sunset-defined-api: + documentationUrl: "https://github.com/entur/api-guidelines/blob/main/guidelines.md#25-lifecycle" + severity: error # Or should this just be a warning? + given: $[?(@property === "info" && @["x-deprecated"] === true)] + then: + - field: x-sunset + function: schema + functionOptions: + schema: + type: string + + entur-sunset-undefined-api: + documentationUrl: "https://github.com/entur/api-guidelines/blob/main/guidelines.md#25-lifecycle" + severity: error + given: $[?(@property === "info" && @["x-deprecated"] !== true)] + then: + field: x-sunset + function: undefined + + # On individual operation level + entur-stability-level-operation: + documentationUrl: "https://github.com/entur/api-guidelines/blob/main/guidelines.md#25-lifecycle" + severity: error + given: $.paths.*[get,post,put,patch,delete,options,head,trace].x-stability-level + then: + function: enumeration + functionOptions: + values: [draft, beta, stable] + + entur-sunset-format-operation: + documentationUrl: "https://github.com/entur/api-guidelines/blob/main/guidelines.md#25-lifecycle" + severity: error + given: $.paths.*[get,post,put,patch,delete,options,head,trace].x-sunset + then: + function: date + + entur-sunset-defined-operation: + documentationUrl: "https://github.com/entur/api-guidelines/blob/main/guidelines.md#254-lifecycle" + severity: error # Or should this just be a warning? + given: $.paths.[?(@.deprecated === true)].x-sunset + then: + function: defined + + entur-sunset-undefined-operation: + documentationUrl: "https://github.com/entur/api-guidelines/blob/main/guidelines.md#25-lifecycle" + severity: error + given: $.paths.[?(@.deprecated !== true)].x-sunset + message: "\"x-sunset\" should only be set when deprecated is true" + then: + function: "undefined" + # ============================================================================= # 3. Naming & Structure Conventions diff --git a/functions/date.js b/functions/date.js new file mode 100644 index 0000000..8a00623 --- /dev/null +++ b/functions/date.js @@ -0,0 +1,14 @@ +module.exports = (targetVal, options, context) => { + if (typeof targetVal !== "string") { + return [{ message: "Must be a valid calendar date."}] + } + if (targetVal.match(/^\d{4}-\d{2}-\d{2}$/) == null) { + return [{ message: "Must be a valid calendar date."}] + } + + if (isNaN(Date.parse(targetVal))) { + return [{ message: "Must be a valid calendar date."}] + } + + return [] +} \ No newline at end of file diff --git a/guidelines.md b/guidelines.md index 39eed63..1e0a542 100644 --- a/guidelines.md +++ b/guidelines.md @@ -263,46 +263,65 @@ Here, only 1 specification will be shown on the Developer Portal, which is a com ### 2.5 Lifecycle -All APIs, and endpoints within an API, follow a lifecycle with different stages that have different properties. +All APIs, and endpoints within an API, follow a lifecycle with different stages that have different properties. The following sections describe a standardized way to declare an API's lifecycle status in its specification. Following this standard has several benefits: +- It is easier for consumers to understand what they can expect from an API in terms of stability. +- It is easier to enforce Enturs common policy on backwards compatibility and deprecation. -1. Design and development: The shape of the API is being figured out. Changes happen often. -2. Testing: Feedback is gathered. Changes happen, and more design and development may be necessary. -3. Stabilization and production use: The API is relied upon by consumers. Changes may happen, as long as they are not breaking. -4. Deprecation: The API is marked as deprecated and consumers are encouraged to move to a new version. Only critical changes will happen. -5. Sunsetting: The API is removed. +Lifecycle status may be declared both for the entire API specification, or for individual operations (endpoints). At the API level, use the following extension fields in the `info` block: -The following sections describe a standardized way to declare an API's lifecycle status in its specification. Following this standard has several benefits: -- It is easier for consumers to understand what they can expect from an API in terms of stability. -- It is easier to enforce Enturs common deprecation rules. +Field name |Type |Description +------------------|---------|----------- +x-stability-level |`string` | The current lifecycle status of the API. Must be one of: `draft`, `beta`, `stable`. Default value is `stable`. +x-deprecated |`boolean`| `true` if the API is deprecated. Default value is `false`. +x-sunset |`string` | The date at which the API will be discontinued, in the format `YYYY-MM-DD`. + +For individual operations, use the following extension fields in an [Operation object](https://spec.openapis.org/oas/v3.1.2.html#operation-object). All of the fields inherit default values from the API level. + +Field name |Type |Description +------------------|---------|----------- +deprecated |`boolean`| Already exists in the OAS. `true` if the endpoint is deprecated. Default value is `info.x-deprecated`. +x-stability-level |`string` | The current lifecycle status of the endpoint. Must be one of: `draft`, `beta`, `stable`. Default value is `info.x-stability-level`. +x-sunset |`string` | The date at which the endpoint will be discontinued, in the format `YYYY-MM-DD`. Default value is `info.x-sunset`. + +Below is a description of what values these fields should have during an API's or endpoint's lifecycle. #### 2.5.1 Design and development +The shape of the API is being figured out. Changes happen often. The API may not even be implemented yet. - :ballot_box_with_check: The field `x-stability-level` **MUST** have the value `draft` -- :ballot_box_with_check: The field `deprecated` **MUST** be empty or have the value `false` +- :ballot_box_with_check: The field `deprecated`/`x-deprecated` **MUST** have the value `false` (or be empty) - :ballot_box_with_check: The field `x-sunset` **MUST** be empty +- :ballot_box_with_check: You **MAY** make [breaking changes](#I-breaking-changes) #### 2.5.2 Testing and feedback +Changes happen, and more design and development may be necessary. - :ballot_box_with_check: The field `x-stability-level` **MUST** have the value `beta` -- :ballot_box_with_check: The field `deprecated` **MUST** be empty or have the value `false` +- :ballot_box_with_check: The field `deprecated`/`x-deprecated` **MUST** have the value `false` (or be empty) - :ballot_box_with_check: The field `x-sunset` **MUST** be empty +- :ballot_box_with_check: You **MAY** make [breaking changes](#I-breaking-changes) #### 2.5.3 Production use -- :ballot_box_with_check: The field `x-stability-level` **MUST** have the value `stable` or be empty -- :ballot_box_with_check: The field `deprecated` **MUST** be empty or have the value `false` +The API is relied upon by consumers. Changes may happen, as long as they are not breaking. +- :ballot_box_with_check: The field `x-stability-level` **MUST** have the value `stable` (or be empty) +- :ballot_box_with_check: The field `deprecated`/`x-deprecated` **MUST** have the value `false` (or be empty) - :ballot_box_with_check: The field `x-sunset` **MUST** be empty -- :eyes: You **MUST NOT** make [breaking changes](#I-breaking-changes) +- :ballot_box_with_check: You **MUST NOT** make [breaking changes](#I-breaking-changes) #### 2.5.4 Deprecation -- :ballot_box_with_check: The field `deprecated` **MUST** have the value `true` +The API is marked as deprecated and consumers are encouraged to move to a new version. Only critical changes will happen. +- :ballot_box_with_check: The field `deprecated`/`x-deprecated` **MUST** have the value `true` - :ballot_box_with_check: The field `x-sunset` **MUST** filled with a date that follows Enturs rules for deprecation. -- :ballot_box_with_check: The field `x-stability-level` **MUST** be empty +- :ballot_box_with_check: The field `x-stability-level` **MUST** have the value `stable` (or be empty) +- :ballot_box_with_check: You **MUST NOT** make [breaking changes](#I-breaking-changes) #### 2.5.5 Sunsetting -- :ballot_box_with_check: When the date in `x-sunset` has passed, the endpoint or API **SHOULD** be removed. Otherwise, the value should be changed. - -#### 2.5.6 *Notes* -- When an entire API has an `x-stability-level` other than `stable`, individual endpoints may not declare an `x-stability-level` -- When an entire API has `x-deprecated: true`, individual endpoints may not declare `x-stability-level` or `deprecated`. -- `deprecated: true` must not be set together with any other value for `x-stability-level` than `stable`. +The API is removed. +- :ballot_box_with_check: When the date in `x-sunset` has passed, the endpoint or API **SHOULD** be removed. If the API is not ready to be removed yet, the value should be updated. + +#### 2.5.6 Notes +- :ballot_box_with_check: Individual endpoints **MAY** override `x-stability-level` to an earlier stage in the lifecycle than the API, but **NOT** later. + - For example, if `stable` is declared at the API level, endpoints may be `draft`, `beta` or `stable`, but if `beta` is declared at the API level, endpoints may only be `draft` or `beta`. +- :ballot_box_with_check: Individual endpoints **MUST NOT** set `deprecated` to `false`, if the API has set `x-deprecated` to `true`. +- :ballot_box_with_check: Individual endpoints **MAY** override `x-sunset` to an earlier date than the API, but **NOT** later. ## 3. Naming & Structure Conventions