From de6b8314bf386f396b9df06d9caa70f400f6f3bd Mon Sep 17 00:00:00 2001
From: Marc Wrobel
Date: Sat, 17 Dec 2022 13:23:05 +0100
Subject: [PATCH] Add API v1 (#2595, #2425, #2331, #2066, #2062, #1762, #759,
#394, #2530)
This is a major rework of the API with a lot of breaking changes. See CHANGELOG_API.md for more information.
Note that we thought of disabling API generation in development (using JEKYLL_ENV like the Jekyll Feed plugin - see https://github.com/jekyll/jekyll-feed/blob/master/lib/jekyll-feed/generator.rb#L145), but it was finally reverted. It does not work well with Netlify preview, and generate production URL (i.e. https://endoflife.date URLs) in development which makes it difficult to use.
---
CHANGELOG_API.md | 96 ++++++
HACKING.md | 8 +-
README.md | 7 +-
_config.yml | 2 +-
_headers | 4 +
_layouts/json.json | 1 +
_layouts/product.html | 4 +-
_layouts/swagger-ui.html | 31 ++
_plugins/generate-api-v1.rb | 342 +++++++++++++++----
_plugins/product-data-enricher.rb | 23 +-
_redirects | 22 +-
api_v1/openapi.yml | 535 ++++++++++++++++++++++++++++++
api_v1/swagger-ui.md | 6 +
assets/404.json | 3 -
humans.txt | 2 +-
index.md | 4 +-
16 files changed, 996 insertions(+), 94 deletions(-)
create mode 100644 CHANGELOG_API.md
create mode 100644 _layouts/json.json
create mode 100644 _layouts/swagger-ui.html
create mode 100644 api_v1/openapi.yml
create mode 100644 api_v1/swagger-ui.md
delete mode 100644 assets/404.json
diff --git a/CHANGELOG_API.md b/CHANGELOG_API.md
new file mode 100644
index 00000000000..3bc9e66b4bd
--- /dev/null
+++ b/CHANGELOG_API.md
@@ -0,0 +1,96 @@
+## API v1.0.0
+
+### Summary
+
+API v1 is a major rework of the API v0 with a lot of breaking changes. Compared to the API v0, API
+v1:
+
+- feels more _Restful_ (#2431),
+- expose almost all product's data (#394, #759, #2062, #2595),
+- expose new metadata such as `schema version` (#2331), `total` (for lists), `generated_at` or
+ `last modified` date,
+- is easier to consume thanks to:
+ - new computed fields such as `is_maintained`,
+ - the replacement of fields that were using union types with two separate single-type fields:
+ - `lts` -> `isLts` and `ltsFrom`,
+ - `support` -> `isActiveSupportOver` and `activeSupportUntil`,
+ - `eol` -> `isEol` and `eolFrom`,
+ - `discontinued` -> `isDiscontinued` and `discontinuedFrom`,
+ - `extendedSupport` -> `isExtendedSupportOver` and `extendedSupportUntil`.
+- provide new endpoints (#2078, #2160, #2530)
+- is versioned using the `api/v1` prefix (#2066), making it easier to implement
+ non-backward-compatible changes in the future,
+- is documented using [swagger-ui](https://swagger.io/tools/swagger-ui/) instead of [Stoplight
+ Elements WebComponent](https://github.com/stoplightio/elements/blob/main/docs/getting-started/elements/html.md)
+ (#905),
+- but reverts #2425 due to incompatibilities in redirect rules.
+
+The API v1 is now generated using a Jekyll Generator (see https://jekyllrb.com/docs/plugins/generators/)
+instead of a custom script.
+
+Note that the API v0 is still generated to give time to users to migrate to API v1. It will be
+decommissioned at least one year after the API v1 release date.
+
+API v1 documentation can be seen on .
+The old API v0 documentation can still be seen on .
+
+### Changes in the "All products" endpoint
+
+- Path has been changed from `api/all.json` to `api/v1/products/`
+- Response has been changed from a simple array of strings to a JSON document.
+ This made it possible to include additional metadata, such as the schema version and the number of
+ products.
+- Response items has been changed from a simple string (the product name) to a JSON document (#2062).
+ This made it possible to include additional information about the product, such as its category
+ and tags.
+- See for a detailed description of the
+ response.
+
+### Changes in the "Product" endpoint
+
+- Path has been changed from `api/.json` to `api/v1/products//`.
+- Response has been changed from a simple array of versions to a JSON document.
+ This made it possible to include :
+ - additional metadata, such as the schema version and the last modified date,
+ - product-level information, such as the product label or category (#2062).
+- Cycles data now always contain all the release cycles properties, even if they are null
+ (example: `discontinued`, `latest`, `latestReleaseDate`, `support`...).
+- See for a detailed
+ description of the response.
+
+### Changes in the "Cycle" endpoint
+
+- Path has been changed from `api//.json` to `api/v1/products//cycles//`.
+- Response has been changed to make it possible to include additional metadata, such as the schema
+ version and the last modified date,
+- Cycles data now always contain all the release cycles properties, even if they are null
+ (example: `discontinued`, `latest`, `latestReleaseDate`, `support`...).
+- A special `/api/v1/products//cycles/latest/` cycle, containing the same data as the
+ latest cycle, has been added (#2078).
+- See for a
+ detailed description of the response.
+
+### Changes in 404 error responses
+
+404 error JSON responses are not returned anymore. #2425 has been reverted because it conflicted
+with the rule that rewrites the paths to add `/index.json` to all requests, which is also a global
+rule and [takes precedence](https://docs.netlify.com/routing/redirects/#rule-processing-order).
+
+### New endpoints
+
+- `/api/v1/categories/`: Get a list of all categories.
+- `/api/v1/categories/`: Get a list of all products within the given category.
+- `/api/v1/tags/`: Get a list of all tags.
+- `/api/v1/tags/`: Get a list of all products having the given tag.
+- `/api/v1/products/full/`: Get a list of all products with all their details (including cycles).
+ This endpoint provides a dump of nearly all the endoflife.date data.
+
+
+
+## API v0
+
+On 2023-03-02 the v0 endpoints were:
+
+- "All products" (`/api/all.json`) : Get a list of all product names.
+- "Product" (`/api/{product}.json`) : Get all release cycles details for a given product.
+- "Cycle" (`/api/{product}/{cycle}.json`) : Get details for a single release cycle of a given product.
diff --git a/HACKING.md b/HACKING.md
index cc1821b3b16..cc8cb0394b2 100644
--- a/HACKING.md
+++ b/HACKING.md
@@ -103,7 +103,13 @@ The API is just JSON files generated in the `api` directory by `_plugins/create-
### API Documentation
-The API Documentation is available at and is generated from an OpenAPI Specification file located at `_data/openapi.yml`. The documentation is rendered [Stoplight Elements](https://meta.stoplight.io/docs/elements/ZG9jOjMyNjU4OTY0-introduction-to-elements).
+The current API v1 documentation is available at and is
+generated from an OpenAPI Specification file located at `api_v1/openapi.yml`. The documentation is
+rendered by [Swagger UI](https://swagger.io/tools/swagger-ui/).
+
+The old API v0 documentation is available at and is
+generated from an OpenAPI Specification file located at `assets/openapi.yml`. The documentation is
+rendered by [Stoplight Elements](https://meta.stoplight.io/docs/elements/ZG9jOjMyNjU4OTY0-introduction-to-elements).
## Contributing Workflow
diff --git a/README.md b/README.md
index 9c34df77bde..97a7cf2fcdd 100644
--- a/README.md
+++ b/README.md
@@ -26,9 +26,8 @@ While participating in the project, you must abide by its [Code of Conduct](CODE
## API
-An API is available for integration with CI platforms.
-API documentation is available at https://endoflife.date/docs/api.
-The API is currently in Alpha, and breaking changes can happen.
+An API is available for integration with CI platforms. API documentation is available at https://endoflife.date/docs/api/v1/.
+The API is currently in Beta, and breaking changes can happen.
## License
@@ -46,6 +45,8 @@ endoflife.date is relying on various amazing software and components :
- [Just the Docs](https://github.com/just-the-docs/just-the-docs), a documentation theme for Jekyll.
- [Stoplight Elements](https://stoplight.io/open-source/elements), a collection of UI components for
displaying beautiful developer documentation from any OpenAPI document.
+- [Swagger UI](https://swagger.io/tools/swagger-ui/), a documentation generator for OpenAPI
+ Specification.
- [Simple Icons](https://simpleicons.org/), free SVG icons for popular brands.
- Our icon is derived from [Hourglass icon (orange)](https://commons.wikimedia.org/wiki/File:Hourglass_icon_%28orange%29.svg)
by David Abián and Serhio Magpie on the English Wikipedia. Remixed under the CC-BY-SA-4.0 license.
diff --git a/_config.yml b/_config.yml
index 708b02b4dd7..de8f4cf8ade 100644
--- a/_config.yml
+++ b/_config.yml
@@ -40,7 +40,7 @@ aux_links:
Source:
- https://github.com/endoflife-date/endoflife.date
API:
- - /docs/api
+ - /docs/api/v1/
"Release Data":
- https://github.com/endoflife-date/release-data/
diff --git a/_headers b/_headers
index 7bb1e154e5e..1c17bb4ade4 100644
--- a/_headers
+++ b/_headers
@@ -56,6 +56,10 @@ layout: null
Content-Security-Policy: default-src 'none'; manifest-src 'self'; connect-src 'self'; script-src 'self'; style-src 'self'; img-src {{ defaultCspImgSrc }} {{ releaseImageSrc }}
Link: /api{{page.permalink}}.json; rel=alternate;type=application/json
Link: /calendar{{page.permalink}}.ics; rel=alternate;type=text/calendar
+ {% elsif page.permalink contains '/docs/api/v' %}
+ {%- comment %}Used contains to match all API version (startswith does not exist){% endcomment %}
+ # unsafe-inline and data: should not be an issue for a static site
+ Content-Security-Policy: default-src 'none'; manifest-src 'self'; connect-src 'self'; script-src 'self' 'unsafe-inline' https://unpkg.com/; style-src 'self' https://unpkg.com/; img-src 'self' data:
{% elsif page.permalink == '/docs/api' %}
Content-Security-Policy: default-src 'none'; manifest-src 'self'; connect-src 'self'; script-src 'self' https://unpkg.com/@stoplight/elements/web-components.min.js; style-src 'self' https://unpkg.com/@stoplight/elements/ 'unsafe-inline'
{% else %}
diff --git a/_layouts/json.json b/_layouts/json.json
new file mode 100644
index 00000000000..8c98299c885
--- /dev/null
+++ b/_layouts/json.json
@@ -0,0 +1 @@
+{{ page.data | jsonify }}
diff --git a/_layouts/product.html b/_layouts/product.html
index 9e1b781e051..21239be4167 100644
--- a/_layouts/product.html
+++ b/_layouts/product.html
@@ -198,7 +198,7 @@ {{ page.title }}
- A JSON version of this page is available at /api{{page.permalink}}.json .
- See the API Documentation for more information.
+ A JSON version of this page is available at /api/v1/products{{page.permalink}}/ .
+ See the API Documentation for more information.
You can subscribe to the iCalendar feed at /calendar{{page.permalink}}.ics .
diff --git a/_layouts/swagger-ui.html b/_layouts/swagger-ui.html
new file mode 100644
index 00000000000..b75f0d38846
--- /dev/null
+++ b/_layouts/swagger-ui.html
@@ -0,0 +1,31 @@
+---
+layout: null
+---
+
+
+
+
+ {{ page.title }}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/_plugins/generate-api-v1.rb b/_plugins/generate-api-v1.rb
index 2f93923af10..f979c5b44cd 100755
--- a/_plugins/generate-api-v1.rb
+++ b/_plugins/generate-api-v1.rb
@@ -1,89 +1,287 @@
-#!/usr/bin/env ruby
-
# This script creates API files for version 1 of the endoflife.date API.
#
-# There are three kind of generated files :
-# - all.json: contains the list of all the product names.
-# - .json: contains a given product data ()including releases data).
-# - /.json: contains a given product release data.
+# There are multiples endpoints :
+#
+# - /api/v1/ - list all major endpoints (those not requiring a parameter)
+# - /api/v1/products/ - list all products
+# - /api/v1/products// - get a single product details
+# - /api/v1/products//latest - get details on the latest cycle for the given product
+# - /api/v1/products// - get details on the given cycle for the given product
+# - /api/v1/categories/ - list categories used on endoflife.date
+# - /api/v1/categories/ - list products having the given category
+# - /api/v1/tags/ - list tags used on endoflife.date
+# - /api/v1/tags/ - list products having the given tag
+
-require 'fileutils'
-require 'json'
-require 'yaml'
-require 'date'
+require 'jekyll'
module ApiV1
- # This API path
- DIR = 'api/v1'
+ VERSION = '1.0.0'
+ MAJOR_VERSION = VERSION.split('.')[0]
+
+ STRIP_HTML_BLOCKS = Regexp.union(
+ //m,
+ //m,
+ //m
+ )
+ STRIP_HTML_TAGS = /<.*?>/m
+
+ # Remove HTML from a string (such as an LTS label).
+ # This is the equivalent of Liquid::StandardFilters.strip_html, which cannot be used
+ # unfortunately.
+ def self.strip_html(input)
+ empty = ''.freeze
+ result = input.to_s.gsub(STRIP_HTML_BLOCKS, empty)
+ result.gsub!(STRIP_HTML_TAGS, empty)
+ result
+ end
+
+ def self.site_url(site, path)
+ "#{site.config['url']}#{path}"
+ end
- # Returns the path of a file inside the API namespace.
- def self.file(name, *args)
- File.join(DIR, name, *args)
+ def self.api_url(site, path)
+ site_url(site, "/api/v#{ApiV1::MAJOR_VERSION}#{path}")
end
- # Holds information about a product.
- Product = Class.new do
- attr_accessor :data
-
- # Initializes the product with the given product's markdown file.
- # The markdown file is expected to contain a YAML front-matter with the appropriate properties.
- #
- # Copying the data makes it easier to process it.
- def initialize(data)
- @data = Hash.new
- # The product name is derived from the product's permalink (ex. /debian => debian).
- @data["name"] = data['permalink'][1..data['permalink'].length]
- @data["title"] = data['title']
- @data["category"] = data['category']
- @data["iconSlug"] = data['iconSlug']
- @data["permalink"] = data['permalink']
- @data["versionCommand"] = data['versionCommand']
- @data["auto"] = data.has_key? 'auto'
- @data["releasePolicyLink"] = data['releasePolicyLink']
- @data["releases"] = data['releases'].map do |release|
- release_data = Hash.new
- release_data["name"] = release['releaseCycle']
- release_data["codename"] = release['codename']
- release_data["releaseDate"] = release['releaseDate']
- release_data["support"] = release['support']
- release_data["eol"] = release['eol']
- release_data["discontinued"] = release['discontinued']
- release_data["lts"] = release['lts'] || false # lts is optional, make sure it always has a value
- release_data["latest"] = release['latest']
- release_data["latestReleaseDate"] = release['latestReleaseDate']
- release_data
+ class ApiGenerator < Jekyll::Generator
+ safe true
+ priority :lowest
+
+ TOPIC = "API " + ApiV1::VERSION + ":"
+
+ def generate(site)
+ @site = site
+ start = Time.now
+ Jekyll.logger.info TOPIC, "Generating..."
+
+ product_pages = site.pages.select { |page| page.data['layout'] == 'product' }
+ add_index_page(site)
+ add_products_related_pages(site, product_pages)
+ add_categories_related_pages(site, product_pages)
+ add_tags_related_pages(site, product_pages)
+
+ Jekyll.logger.info TOPIC, "Done in #{(Time.now - start).round(3)} seconds."
+ end
+
+ private
+
+ def add_index_page(site)
+ site.pages << JsonPage.of_raw_data(site, '/', [
+ { name: "products", uri: "#{ApiV1.api_url(site, '/products/')}" },
+ { name: "categories", uri: "#{ApiV1.api_url(site, '/categories/')}" },
+ { name: "tags", uri: "#{ApiV1.api_url(site, '/tags/')}" },
+ ])
+ end
+
+ def add_products_related_pages(site, products)
+ add_all_products_page(site, products)
+ add_all_products_and_cycles_page(site, products)
+
+ products.each do |page|
+ add_product_page(site, page)
+ add_latest_cycle_page(site, page)
+ page.data['releases'].each { |cycle| add_cycle_page(site, page, cycle) }
+ end
+ end
+
+ def add_all_products_page(site, products)
+ site.pages << JsonPage.of_products_summary(site, '/products/', products)
+ end
+
+ def add_all_products_and_cycles_page(site, products)
+ site.pages << JsonPage.of_products_details(site, '/products/full/', products)
+ end
+
+ def add_product_page(site, product)
+ site.pages << JsonPage.of_product(site, product)
+ end
+
+ def add_latest_cycle_page(site, page)
+ latest = page.data['releases'][0]
+ site.pages << JsonPage.of_cycle(site, page, latest, 'latest')
+ end
+
+ def add_cycle_page(site, page, cycle)
+ site.pages << JsonPage.of_cycle(site, page, cycle)
+ end
+
+ def add_categories_related_pages(site, products)
+ products_by_category = products_by_category(products)
+
+ add_all_categories_page(site, products_by_category.keys)
+ products_by_category.each do |category, products|
+ add_category_page(site, category, products)
+ end
+ end
+
+ def products_by_category(products)
+ products_by_category = {}
+ products.each { |product| add_to_map(products_by_category, product.data['category'], product) }
+ products_by_category
+ end
+
+ def add_category_page(site, category, products)
+ site.pages << JsonPage.of_products_summary(site, "/categories/#{category}", products)
+ end
+
+ def add_all_categories_page(site, categories)
+ data = categories.map { |category| { name: category, uri: "#{ApiV1.api_url(site, "/categories/#{category}/")}" }}
+ meta = { total: categories.size() }
+ site.pages << JsonPage.of_raw_data(site, '/categories/', data, meta)
+ end
+
+ def add_tags_related_pages(site, products)
+ products_by_tag = products_by_tag(products)
+
+ add_all_tags_page(site, products_by_tag.keys)
+ products_by_tag.each do |tag, products|
+ add_tag_page(site, tag, products)
end
end
- def name
- data["name"]
+ def products_by_tag(products)
+ products_by_tag = {}
+ products.each do |product|
+ product.data['tags'].each { |tag| add_to_map(products_by_tag, tag, product) }
+ end
+ products_by_tag
+ end
+
+ def add_tag_page(site, tag, products)
+ site.pages << JsonPage.of_products_summary(site, "/tags/#{tag}", products)
+ end
+
+ def add_all_tags_page(site, tags)
+ data = tags.map { |tag| { name: tag, uri: "#{ApiV1.api_url(site, "/tags/#{tag}/")}" }}
+ meta = { total: tags.size() }
+ site.pages << JsonPage.of_raw_data(site, '/tags/', data, meta)
+ end
+
+ def add_to_map(map, key, page)
+ if map.has_key? key
+ map[key] << page
+ else
+ map[key] = [page]
+ end
end
end
-end
-product_names = []
-FileUtils.mkdir_p(ApiV1::file('.'))
-
-Dir['products/*.md'].each do |file|
- # Load and prepare data
- raw_data = YAML.safe_load(File.open(file), permitted_classes: [Date])
- product = ApiV1::Product.new(raw_data)
- product_names.append(product.name)
-
- # Write /.json
- product_file = ApiV1::file("#{product.name}.json")
- File.open(product_file, 'w') { |f| f.puts product.data.to_json }
-
- # Write all //.json
- FileUtils.mkdir_p(ApiV1::file(product.name))
- product.data["releases"].each do |release|
- # Any / characters in the name are replaced with - to avoid file errors.
- release_file = ApiV1::file(product.name, "#{release['name'].to_s.tr('/', '-')}.json")
- File.open(release_file, 'w') { |f| f.puts release.to_json }
+ class JsonPage < Jekyll::Page
+ class << self
+ private :new
+
+ def of_raw_data(site, path, data, metadata = {})
+ new(site, path, data, metadata)
+ end
+
+ def of_products_summary(site, path, products)
+ data = products.map { |product| product_summary_to_json(site, product) }
+ meta = { total: products.size() }
+ new(site, path, data, meta)
+ end
+
+ def of_products_details(site, path, products)
+ data = products.map { |product| product_to_json(site, product) }
+ meta = { total: products.size() }
+ new(site, path, data, meta)
+ end
+
+ def of_product(site, product)
+ path = "/products/#{product.data['id']}"
+ data = product_to_json(site, product)
+ meta = {
+ # https://github.com/gjtorikian/jekyll-last-modified-at/blob/master/lib/jekyll-last-modified-at/determinator.rb
+ last_modified: product.data['last_modified_at'].last_modified_at_time.iso8601,
+ auto: product.data.has_key?('auto'),
+ }
+ new(site, path, data, meta)
+ end
+
+ def of_cycle(site, product, cycle, identifier = nil)
+ name = identifier ? identifier : cycle['id']
+ path = "/products/#{product.data['id']}/cycles/#{name}"
+ data = cycle_to_json(cycle)
+ new(site, path, data, {})
+ end
+
+ def product_to_json(site, product)
+ additional_details = {
+ versionCommand: product.data['versionCommand'],
+ identifiers: product.data['identifiers'].map { |identifier| {
+ type: identifier.keys.first,
+ id: identifier.values.first
+ } },
+ labels: {
+ "activeSupport": product.data['activeSupportColumn'] ? ApiV1.strip_html(product.data['activeSupportColumnLabel']) : nil,
+ "discontinued": product.data['discontinuedColumn'] ? ApiV1.strip_html(product.data['discontinuedColumnLabel']) : nil,
+ "eol": product.data['eolColumn'] ? ApiV1.strip_html(product.data['eolColumnLabel']) : nil,
+ "extendedSupport": product.data['extendedSupportColumn'] ? ApiV1.strip_html(product.data['extendedSupportColumnLabel']) : nil,
+ },
+ links: {
+ icon: product.data['iconUrl'],
+ html: ApiV1.site_url(site, "/#{product.data['id']}"),
+ releasePolicy: product.data['releasePolicyLink'],
+ },
+ cycles: product.data['releases'].map { |cycle| cycle_to_json(cycle) }
+ }
+
+ product_summary_to_json(site, product).except(:uri).merge(additional_details)
+ end
+
+ def product_summary_to_json(site, product)
+ {
+ name: product.data['id'],
+ aliases: product.data['aliases'],
+ label: product.data['title'],
+ category: product.data['category'],
+ tags: product.data['tags'],
+ uri: ApiV1.api_url(site, "/products/#{product.data['id']}/")
+ }
+ end
+
+ def cycle_to_json(cycle)
+ {
+ name: cycle['releaseCycle'],
+ codename: cycle['codename'],
+ label: ApiV1.strip_html(cycle['label']),
+ date: cycle['releaseDate'],
+ isLts: cycle['is_lts'],
+ ltsFrom: cycle['lts_from'],
+ isActiveSupportOver: cycle['is_active_support_over'],
+ activeSupportUntil: cycle['active_support_until'],
+ isEol: cycle['is_eol'],
+ eolFrom: cycle['eol_from'],
+ isDiscontinued: cycle['is_discontinued'],
+ discontinuedFrom: cycle['discontinued_from'],
+ isExtendedSupportOver: cycle['is_extended_support_over'],
+ extendedSupportUntil: cycle['extended_support_until'],
+ isMaintained: cycle['is_maintained'],
+ latest: {
+ name: cycle['latest'],
+ date: cycle['latestReleaseDate'],
+ link: cycle['link'],
+ }
+ }
+ end
+ end
+
+ def initialize(site, path, data, metadata)
+ @site = site
+ @base = site.source
+ @dir = "api/v#{ApiV1::MAJOR_VERSION}#{path}"
+ @name = "index.json"
+ @data = {}
+ @data['layout'] = 'json'
+
+ @data['data'] = {}
+ @data['data']['schema_version'] = ApiV1::VERSION
+ @data['data']['generated_at'] = site.time.iso8601
+ @data['data'].merge!(metadata)
+ @data['data']['result'] = data
+
+ self.process(@name)
+ end
end
end
-
-# Write /all.json
-all_products_file = ApiV1::file('all.json')
-File.open(all_products_file, 'w') { |f| f.puts product_names.sort.to_json }
diff --git a/_plugins/product-data-enricher.rb b/_plugins/product-data-enricher.rb
index 4919d64d04f..d2672d32178 100644
--- a/_plugins/product-data-enricher.rb
+++ b/_plugins/product-data-enricher.rb
@@ -36,6 +36,8 @@ def enrich(page)
set_description(page)
set_icon_url(page)
set_tags(page)
+ set_identifiers(page)
+ set_aliases(page)
set_overridden_columns_label(page)
page.data["releases"].each { |release| enrich_release(page, release) }
@@ -84,6 +86,23 @@ def set_tags(page)
page.data['tags'] = tags.sort
end
+ # Set alias (derived from alternate_urls).
+ def set_aliases(page)
+ if page.data['alternate_urls']
+ page.data['aliases'] = page.data['alternate_urls'].map { |path| path[1..] }
+ else
+ page.data['alternate_urls'] = [] # should be in a separate method, but easier that way
+ page.data['aliases'] = []
+ end
+ end
+
+ # Set identifiers to empty if it's not present.
+ def set_identifiers(page)
+ if !page.data['identifiers']
+ page.data['identifiers'] = []
+ end
+ end
+
# Set properly the column presence/label if it was overridden.
def set_overridden_columns_label(page)
date_column_names = %w[releaseDateColumn releaseColumn discontinuedColumn eoasColumn eolColumn eoesColumn]
@@ -321,8 +340,10 @@ def days_toward_now(date)
return (date_timestamp - now_timestamp) / (60 * 60 * 24)
end
+ # Template rendering function that replaces placeholders.
+ # The template is stripped to avoid unnecessary whitespaces in the output.
def render_eol_template(template, cycle)
- link = template.gsub('__RELEASE_CYCLE__', cycle['releaseCycle'] || '')
+ link = template.strip().gsub('__RELEASE_CYCLE__', cycle['releaseCycle'] || '')
link.gsub!('__CODENAME__', cycle['codename'] || '')
link.gsub!('__LATEST__', cycle['latest'] || '')
link.gsub!('__LATEST_RELEASE_DATE__', cycle['latestReleaseDate'] ? cycle['latestReleaseDate'].iso8601 : '')
diff --git a/_redirects b/_redirects
index 661ebe047d2..b4a92b88ce0 100644
--- a/_redirects
+++ b/_redirects
@@ -9,35 +9,41 @@
# Setting a layout forces Jekyll to render this file
layout: null
---
-{%- for page in site.pages -%}
+{% for page in site.pages -%}
# Redirects for {{page.path}}
{%- if page.alternate_urls %}
{%- for url in page.alternate_urls %}
{{url}} {{page.permalink}}
{%- endfor %}
{%- endif %}
- {%- if page.layout == 'product' %}
+ {%- if page.layout == 'product' %}
{{page.permalink}}/_edit https://github.com/endoflife-date/endoflife.date/edit/master/{{page.path}}
+{%- comment %}API v0 redirect{% endcomment %}
/api{{page.permalink}} /api{{page.permalink}}.json
{%- if page.alternate_urls %}
{%- for url in page.alternate_urls %}
/calendar{{url}}.ics /calendar{{page.permalink}}.ics
/api{{url}}.json /api{{page.permalink}}.json
/api{{url}}/* /api{{page.permalink}}/:splat
+/api/v1/products{{url}}/* /api/v1/products{{page.permalink}}/:splat
{%- endfor %}
{%- endif %}
{%- endif %}
{% endfor %}
-# Clients will try to access /favicon.ico, in some scenarios
-# we don't want the file in our codebase, because the theme
-# embeds it as a favicon, so instead set a redirect for
-# these clients to a PNG file instead.
+# Clients will try to access /favicon.ico, in some scenarios we don't want the file in our codebase,
+# because the theme embeds it as a favicon, so instead set a redirect for these clients to a PNG file instead.
/favicon.ico /assets/favicon-32x32.png
-# Send API 404 responses in JSON
-/api/* /assets/404.json 404
+# Rewrite for /api/v1/ to keep URLs clean.
+# All API responses are located in an index.json and must be accessible without the file name, such as:
+# - /api/v1/index.json -> /api/v1/
+# - /api/v1/products/almalinux/index.json -> /api/v1/products/almalinux/
+# This uses shadowing : https://docs.netlify.com/routing/redirects/rewrites-proxies/#shadowing, and
+# it must be declared at the end of the file to not take precedence on the redirects (see
+# https://docs.netlify.com/routing/redirects/#rule-processing-order).
+/api/v1/* /api/v1/:splat/index.json 200!
# A few permanent redirects for removed pages
/tags/api-gateway /tags/web-server
diff --git a/api_v1/openapi.yml b/api_v1/openapi.yml
new file mode 100644
index 00000000000..4fae130bba3
--- /dev/null
+++ b/api_v1/openapi.yml
@@ -0,0 +1,535 @@
+---
+# API v1 description. See https://spec.openapis.org/oas/v3.1.0 for specification.
+# Edit using https://editor.swagger.io/.
+
+permalink: /docs/api/v1/openapi.yml
+layout: null
+---
+openapi: 3.0.3
+
+info:
+ title: endoflife API
+ version: "1.0.0-b1"
+ license:
+ name: MIT License
+ url: 'https://github.com/endoflife-date/endoflife.date/blob/master/LICENSE'
+ description: >-
+ endoflife.date documents EOL dates and support lifecycles for various products.
+ The endoflife API allows users to discover and query for those products.
+
+
+ Some useful links:
+
+ - [The endoflife.date website](https://endoflife.date/)
+
+ - [The endoflife.date repository](https://github.com/endoflife-date/endoflife.date)
+
+ - [The endoflife.date issue tracker](https://github.com/endoflife-date/endoflife.date/issues/)
+
+ - [The source API definition](https://github.com/endoflife-date/endoflife.date/blob/master/assets/openapi.yml)
+
+# Replace with your preview URL (such as https://deploy-preview-2080--endoflife-date.netlify.app/api/v1).
+servers:
+ - url: {{ site.url }}/api/v1
+
+paths:
+ /:
+ get:
+ summary: List the main endoflife.date API endpoints.
+ responses:
+ '200':
+ description: successful operation
+ headers: {}
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/UriListResponse'
+
+ /products:
+ get:
+ summary: >
+ List all the products referenced on endoflife.date.
+ Only a subset of each product's data is returned by this endpoint.
+ responses:
+ '200':
+ description: successful operation
+ headers: {}
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ProductListResponse'
+
+ /products/full/:
+ get:
+ summary: >
+ List all the products referenced on endoflife.date, with all their details.
+ The full products data is returned by this endpoint, making the result a dump of nearly all
+ endoflife.date data.
+ responses:
+ '200':
+ description: successful operation
+ headers: {}
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/FullProductListResponse'
+
+ /products/{product}/:
+ get:
+ summary: >
+ Get the given product data.
+ This endpoint is returning all endoflife.date knows about the product, including release
+ cycles data.
+ parameters:
+ - name: product
+ in: path
+ description: 'The name of the product.'
+ required: true
+ schema:
+ type: string
+ responses:
+ '200':
+ description: successful operation
+ headers: {}
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ProductResponse'
+
+ /products/{product}/cycles/{cycle}:
+ get:
+ summary: Get the given product release cycle data.
+ parameters:
+ - name: product
+ in: path
+ description: 'The name of the product.'
+ required: true
+ schema:
+ type: string
+ - name: cycle
+ in: path
+ description: 'The name of the cycle.'
+ required: true
+ schema:
+ type: string
+ responses:
+ '200':
+ description: successful operation
+ headers: {}
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ProductCycleResponse'
+
+ /products/{product}/cycles/latest/:
+ get:
+ summary: Get the latest release cycle data for the given product.
+ parameters:
+ - name: product
+ in: path
+ description: 'The name of the product.'
+ required: true
+ schema:
+ type: string
+ responses:
+ '200':
+ description: successful operation
+ headers: {}
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ProductCycleResponse'
+
+ /categories:
+ get:
+ summary: List all endoflife.date categories.
+ responses:
+ '200':
+ description: successful operation
+ headers: {}
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/UriListResponse'
+
+ /categories/{category}:
+ get:
+ summary: >
+ List all the products referenced on endoflife.date for the given category.
+ Only a subset of each product's data is returned by this endpoint.
+ parameters:
+ - name: category
+ in: path
+ description: 'The name of the category.'
+ required: true
+ schema:
+ type: string
+ responses:
+ '200':
+ description: successful operation
+ headers: {}
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ProductListResponse'
+
+ /tags:
+ get:
+ summary: List all endoflife.date tags.
+ responses:
+ '200':
+ description: successful operation
+ headers: {}
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/UriListResponse'
+
+ /tags/{tag}:
+ get:
+ summary: >
+ List all the products referenced on endoflife.date for the given tag.
+ Only a subset of each product's data is returned by this endpoint.
+ parameters:
+ - name: tag
+ in: path
+ description: 'The name of the tag.'
+ required: true
+ schema:
+ type: string
+ responses:
+ '200':
+ description: successful operation
+ headers: {}
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ProductListResponse'
+
+# Responses must be at the end of the list, contain a schema_version property and be suffixed with
+# 'Response' to facilitate maintenance and reading.
+components:
+ schemas:
+ Uri:
+ type: object
+ properties:
+ name:
+ type: string
+ description: Name of the URI
+ example: tags
+ uri:
+ type: string
+ format: uri
+ description: URI
+ example: {{ site.url }}/tags/
+
+ Identifier:
+ type: object
+ properties:
+ name:
+ type: type
+ description: Type of the identifier (types as of 2023-03 are repology, purl and cpe)
+ example: cpe
+ id:
+ type: string
+ description: Identifier
+ example: cpe:/o:canonical:ubuntu_linux
+
+ ProductVersion:
+ type: object
+ properties:
+ name:
+ type: string
+ description: Name of the version.
+ example: "22.04.2"
+ date:
+ type: string
+ format: date
+ description: Release date.
+ example: "2022-04-21"
+ link:
+ type: string
+ format: uri
+ description: Link to the changelog or release notes.
+ example: https://wiki.ubuntu.com/JammyJellyfish/ReleaseNotes/
+
+ ProductCycle:
+ type: object
+ properties:
+ name:
+ type: string
+ description: Name of the product cycle.
+ example: "22.04"
+ codename:
+ type: string
+ nullable: true
+ description: Name of the product cycle.
+ example: Jammy Jellyfish
+ label:
+ type: string
+ description: Label of the product cycle.
+ example: 22.04 'Jammy Jellyfish' (LTS)
+ date:
+ type: string
+ format: date
+ description: Release date of the cycle.
+ example: "2022-04-21"
+ isLts:
+ ype: boolean
+ description: Whether the product cycle is LTS.
+ example: true
+ ltsFrom:
+ type: string
+ format: date
+ nullable: true
+ description: Start date of the LTS phase.
+ example: "2022-04-21"
+ isActiveSupportOver:
+ type: boolean
+ nullable: true
+ description: Whether the active support phase is over.
+ example: false
+ activeSupportUntil:
+ type: string
+ format: date
+ nullable: true
+ description: End date of the active support phase.
+ example: "2024-09-30"
+ isEol:
+ type: boolean
+ nullable: true
+ description: Whether the product cycle is EOL.
+ example: false
+ eolFrom:
+ type: string
+ format: date
+ nullable: true
+ description: End of life date for the product cycle.
+ example: "2027-04-01"
+ isDiscontinued:
+ type: boolean
+ nullable: true
+ description: Whether the product cycle is discontinued (mainly used for hardware).
+ example: false
+ discontinuedFrom:
+ type: string
+ format: date
+ nullable: true
+ description: Discontinuation date (mainly used for hardware).
+ example: "2027-04-01"
+ isExtendedSupportOver:
+ type: boolean
+ nullable: true
+ description: Whether the extended support phase is over.
+ example: true
+ extendedSupportUntil:
+ type: string
+ format: date
+ nullable: false
+ description: End date of the extended support phase.
+ example: "2032-04-09"
+ isMaintained:
+ type: boolean
+ nullable: false
+ description: Whether or not this cycle still have some level of support. This could be any level of support, even extended support.
+ example: true
+ latest:
+ type: object
+ $ref: '#/components/schemas/ProductVersion'
+
+ ProductSummary:
+ type: object
+ properties:
+ name:
+ type: string
+ description: Name of the product
+ example: ubuntu
+ label:
+ type: string
+ description: Label of the product
+ example: Ubuntu
+ aliases:
+ type: array
+ description: Aliases declared for the product (derived from alternate_urls)
+ items:
+ type: string
+ category:
+ type: string
+ description: Category of the product
+ example: os
+ tags:
+ type: array
+ description: Tags associated to the product
+ items:
+ type: string
+ uri:
+ type: string
+ format: uri
+ description: Link to the full product details
+ example: {{ site.url }}/api/v1/products/ubuntu/
+
+ ProductDetails:
+ type: object
+ properties:
+ name:
+ type: string
+ description: Name of the product
+ example: ubuntu
+ label:
+ type: string
+ description: Label of the product
+ example: Ubuntu
+ aliases:
+ type: array
+ description: Aliases declared for the product (derived from alternate_urls)
+ items:
+ type: string
+ category:
+ type: string
+ description: Category of the product
+ example: os
+ tags:
+ type: array
+ description: Tags associated to the product
+ items:
+ type: string
+ # Additional properties (compared to ProductSummary)
+ versionCommand:
+ type: string
+ description: Command that can be used to check the current product version.
+ example: lsb_release --release
+ identifiers:
+ type: array
+ description: Known identifiers (purl, repology, cpe...) associated to the product
+ items:
+ $ref: '#/components/schemas/Identifier'
+ labels:
+ type: object
+ description: Product labels.
+ properties:
+ activeSupport:
+ type: string
+ nullable: true
+ description: Label used to denote the active support phase.
+ example: Hardware & Maintenance
+ discontinued:
+ type: string
+ nullable: true
+ description: Label used to denote the discontinuation of the product.
+ example: Discontinued
+ eol:
+ type: string
+ nullable: true
+ description: Label used to denote the phase before the EOL of the product.
+ example: Maintenance & Security Support
+ extendedSupport:
+ type: string
+ nullable: true
+ description: Label used to denote the extended support phase.
+ example: Extended Security Maintenance
+ links:
+ type: object
+ description: Product links.
+ properties:
+ icon:
+ type: string
+ format: uri
+ nullable: true
+ description: Link to the product icon (on https://simpleicons.org/).
+ example: https://simpleicons.org/icons/ubuntu.svg
+ html:
+ type: string
+ format: uri
+ description: Link to the product page on endoflife.date.
+ example: https://endoflife.date/ubuntu
+ releasePolicy:
+ type: string
+ format: uri
+ nullable: true
+ description: Link to the product release policy.
+ example: https://wiki.ubuntu.com/Releases
+ cycles:
+ type: array
+ description: Product release cycles.
+ items:
+ $ref: '#/components/schemas/ProductCycle'
+
+ UriListResponse:
+ type: object
+ properties:
+ schema_version:
+ type: string
+ description: Version of this schema.
+ example: 1.0.0
+ total:
+ type: integer
+ format: int32
+ description: Number of uri in the list.
+ example: 3
+ result:
+ type: array
+ items:
+ $ref: '#/components/schemas/Uri'
+
+ ProductListResponse:
+ type: object
+ properties:
+ schema_version:
+ type: string
+ description: Version of this schema.
+ example: 1.0.0
+ total:
+ type: integer
+ format: int32
+ description: Number of products in the list.
+ example: 200
+ result:
+ type: array
+ items:
+ $ref: '#/components/schemas/ProductSummary'
+
+ FullProductListResponse:
+ type: object
+ properties:
+ schema_version:
+ type: string
+ description: Version of this schema.
+ example: 1.0.0
+ total:
+ type: integer
+ format: int32
+ description: Number of products in the list.
+ example: 200
+ result:
+ type: array
+ items:
+ $ref: '#/components/schemas/ProductDetails'
+
+ ProductCycleResponse:
+ type: object
+ properties:
+ schema_version:
+ type: string
+ description: Version of this schema.
+ example: 1.0.0
+ result:
+ $ref: '#/components/schemas/ProductCycle'
+
+ ProductResponse:
+ type: object
+ properties:
+ schema_version:
+ type: string
+ description: Version of this schema.
+ example: 1.0.0
+ last_modified:
+ type: string
+ format: date-time
+ description: The time this product was last modified.
+ example: 2023-03-01T14:05:52+01:00
+ auto:
+ type: boolean
+ description: Whether or not product versions are automatically updated.
+ example: true
+ result:
+ $ref: '#/components/schemas/ProductDetails'
diff --git a/api_v1/swagger-ui.md b/api_v1/swagger-ui.md
new file mode 100644
index 00000000000..bd2a634e7b0
--- /dev/null
+++ b/api_v1/swagger-ui.md
@@ -0,0 +1,6 @@
+---
+title: EndOfLife API v1 Swagger UI
+permalink: /docs/api/v1/
+openapi_yml: /docs/api/v1/openapi.yml
+layout: swagger-ui
+---
diff --git a/assets/404.json b/assets/404.json
deleted file mode 100644
index 0238a1f9945..00000000000
--- a/assets/404.json
+++ /dev/null
@@ -1,3 +0,0 @@
-{
- "message": "Product not found"
-}
\ No newline at end of file
diff --git a/humans.txt b/humans.txt
index 33ff45aba20..4843c09bdbd 100644
--- a/humans.txt
+++ b/humans.txt
@@ -5,5 +5,5 @@ Contributors: https://github.com/endoflife-date/endoflife.date/graphs/contributo
/* SITE */
Software: Jekyll, Netlify, GitHub, Ruby, GitHub Actions
-Components: Just the Docs Jekyll Theme, Stoplight Elements, Simple Icons
+Components: Just the Docs Jekyll Theme, Stoplight Elements, Swagger UI, Simple Icons
Logo: adaptation of "An hourglass in a round icon" by David Abián and Serhio Magpie (https://commons.wikimedia.org/wiki/File:Hourglass_icon_%28orange%29.svg)
diff --git a/index.md b/index.md
index 3776fcbe14c..6bfd22124bf 100644
--- a/index.md
+++ b/index.md
@@ -11,7 +11,7 @@ End-of-life (EOL) and support information is [often hard to track, or very badly
endoflife.date documents EOL dates and support lifecycles for various products.
endoflife.date aggregates data from various sources and presents it in an understandable and
-succinct manner. It also makes the data available using an [easily accessible API](https://endoflife.date/docs/api)
+succinct manner. It also makes the data available using an [easily accessible API](/docs/api/v1/)
and has iCalendar support.
endoflife.date currently tracks {{ site.pages | where: "layout", "product" | size }} products.
@@ -41,7 +41,7 @@ If you maintain release information for a product (end-of-life dates or support
also have a [set of recommendations](/recommendations) along with a checklist on some best practices
for publishing this information.
-And do not hesitate to [play with our API](https://endoflife.date/docs/api). Here are a few awesome
+And do not hesitate to [play with our API](/docs/api/v1/). Here are a few awesome
tools that already did it: [norwegianblue](https://github.com/hugovk/norwegianblue),
[end_of_life](https://github.com/MatheusRich/end_of_life), and
[cicada](https://github.com/mcandre/cicada). Find more on