diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 7fbceeb1fa..4c74067585 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -46,7 +46,7 @@ variables: CI_TARGET_TEST_CONFIG_FILE: "$CI_PROJECT_DIR/tools/ci/config/target-test.yml" # Versioned esp-idf-doc env image to use for all document building jobs - ESP_IDF_DOC_ENV_IMAGE: "$CI_DOCKER_REGISTRY/esp-idf-doc-env:v5" + ESP_IDF_DOC_ENV_IMAGE: "$CI_DOCKER_REGISTRY/esp-idf-doc-env:v6" # before each job, we need to check if this job is filtered by bot stage/job filter diff --git a/SUPPORT_POLICY.md b/SUPPORT_POLICY.md index eed44025da..ec8a68e29e 100644 --- a/SUPPORT_POLICY.md +++ b/SUPPORT_POLICY.md @@ -25,7 +25,7 @@ reserved for features which are needed to meet particular regulatory requirements or standards for new products, and which carry a very low risk of introducing regressions.) -During the Maintenace period, the version is still supported but only bugfixes +During the Maintenance period, the version is still supported but only bugfixes for high severity issues or security issues will be applied. Using an “In Service” version is recommended when starting a new project. diff --git a/docs/chart.png b/docs/chart.png deleted file mode 100644 index 25475313af..0000000000 Binary files a/docs/chart.png and /dev/null differ diff --git a/docs/conf_common.py b/docs/conf_common.py index dd4cc9ee20..61024719ef 100644 --- a/docs/conf_common.py +++ b/docs/conf_common.py @@ -54,6 +54,7 @@ extensions = ['breathe', 'sphinxcontrib.nwdiag', 'sphinxcontrib.rackdiag', 'sphinxcontrib.packetdiag', + 'sphinxcontrib.cairosvgconverter', 'extensions.html_redirects', 'extensions.toctree_filter', diff --git a/docs/en/versions.rst b/docs/en/versions.rst index 6abec3dabf..b2b6730050 100644 --- a/docs/en/versions.rst +++ b/docs/en/versions.rst @@ -70,7 +70,49 @@ As a general guideline: - If possible, periodically update the project to a new major or minor ESP-IDF version (for example, once a year.) The update process should be straightforward for Minor updates, but may require some planning and checking of the release notes for Major updates. - Always plan to update to a newer release before the release you are using becomes End of Life. -.. image:: ../chart.png +Each ESP-IDF major and minor release (V4.1, V4.2, etc) is supported for +30 months after the initial stable release date. + +Supported means that the ESP-IDF team will continue to apply bug fixes, +security fixes, etc to the release branch on GitHub, and periodically +make new bugfix releases as needed. + +Support period is divided into "Service" and "Maintenance" period: + ++-------------+--------------+---------------------------------------+ +| Period | Duration | Recommended for new projects? | ++=============+==============+=======================================+ +| Service | 12 months | Yes | ++-------------+--------------+---------------------------------------+ +| Maintenance | 18 months | No | ++-------------+--------------+---------------------------------------+ + +During the Service period, bugfixes releases are more frequent. In some cases, +support for new features may be added during the Service period (this is +reserved for features which are needed to meet particular regulatory +requirements or standards for new products, and which carry a very low risk of +introducing regressions.) + +During the Maintenance period, the version is still supported but only bugfixes +for high severity issues or security issues will be applied. + +Using an “In Service” version is recommended when starting a new project. + +Users are encouraged to upgrade all projects to a newer ESP-IDF release before +the support period finishes and the release becomes End of Life (EOL). It is our +policy to not continue fixing bugs in End of Life releases. + +Pre-release versions (betas, previews, `-rc` and `-dev` versions, etc) +are not covered by any support period. Sometimes a particular feature is +marked as "Preview" in a release, which means it is also not covered +by the support period. + +The ESP-IDF Programming Guide has information about the +`different versions of ESP-IDF `_ +(major, minor, bugfix, etc). + + +.. image:: https://dl.espressif.com/dl/esp-idf/support-periods.svg Checking the Current Version diff --git a/docs/generate_chart.py b/docs/generate_chart.py new file mode 100644 index 0000000000..6d855b53cd --- /dev/null +++ b/docs/generate_chart.py @@ -0,0 +1,216 @@ +#!/usr/bin/env python + +# Copyright 2020 Espressif Systems (Shanghai) PTE LTD +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +import argparse +import datetime as dt +import json + +import numpy as np +import requests +import matplotlib.dates +import matplotlib.patches as mpatches +import matplotlib.pyplot as plt +from matplotlib.dates import MONTHLY, DateFormatter, RRuleLocator, rrulewrapper +from dateutil import parser +from dateutil.relativedelta import relativedelta + + +class Version(object): + def __init__(self, version_name, explicit_start_date, explicit_end_date, explicit_end_service_date=None): + self.version_name = version_name + + self._start_date = parser.parse(explicit_start_date) + self._end_of_life_date = parser.parse(explicit_end_date) + self._end_service_date = parser.parse( + explicit_end_service_date) if explicit_end_service_date is not None else self.compute_end_service_date() + + self.start_date_matplotlib_format = matplotlib.dates.date2num(self._start_date) + self.end_of_life_date_matplotlib_format = matplotlib.dates.date2num(self._end_of_life_date) + + self.end_service_date_matplotlib_format = matplotlib.dates.date2num(self._end_service_date) + + @staticmethod + def add_months(source_date, months): + return source_date + relativedelta(months=+months) + + def get_start_date(self): + return self._start_date + + def get_end_of_life_date(self): + return self._end_of_life_date + + def get_end_service_date(self): + return self._end_service_date + + def compute_end_service_date(self): + return self.add_months(self._start_date, 12) + + +class ChartVersions(object): + def __init__(self, url=None, filename=None): + self._releases = self._get_releases_from_url(url=url, filename=filename) + self.sorted_releases_supported = sorted(self.filter_old_versions(self._releases), key=lambda x: x.version_name, + reverse=True) + + def get_releases_as_json(self): + return { + x.version_name: { + "start_date": x.get_start_date().strftime("%Y-%m-%d"), + "end_service": x.get_end_service_date().strftime("%Y-%m-%d"), + "end_date": x.get_end_of_life_date().strftime("%Y-%m-%d") + } for x in self.sorted_releases_supported + } + + @staticmethod + def parse_chart_releases_from_js(js_as_string): + return json.loads(js_as_string[js_as_string.find("RELEASES: ") + len("RELEASES: "):js_as_string.rfind("};")]) + + def _get_all_version_from_url(self, url=None, filename=None): + releases_file = requests.get(url).text if url is not None else "".join(open(filename).readlines()) + return self.parse_chart_releases_from_js(releases_file) + + def _get_releases_from_url(self, url=None, filename=None): + all_versions = self._get_all_version_from_url(url, filename) + return [ + Version(version_name=x, + explicit_start_date=all_versions[x]['start_date'], + explicit_end_date=all_versions[x]['end_date'] if 'end_date' in all_versions[x].keys() else None, + explicit_end_service_date=all_versions[x]['end_service'] if 'end_service' in all_versions[ + x].keys() else None) + for x in all_versions.keys() + ] + + @staticmethod + def filter_old_versions(versions): + return list( + filter(lambda x: x.get_end_of_life_date() >= dt.datetime.now(x.get_end_of_life_date().tzinfo), versions)) + + @staticmethod + def months_timedelta(datetime_1, datetime2): + datetime_1, datetime2 = (datetime2, datetime_1) if datetime_1 > datetime2 else (datetime_1, datetime2) + return (datetime2.year * 12 + datetime2.month) - (datetime_1.year * 12 + datetime_1.month) + + @staticmethod + def find_next_multiple_of_power_two(number, initial=3): + """ + Computes the next multiple of the number by some power of two. + >>> ChartVersions.find_next_multiple_of_power_two(7, 3) + 12 + """ + msb = number.bit_length() + return 3 if number <= 1 else initial << msb - 2 << (1 & number >> msb - 2) + + def find_nearest_multiple_of_power_two(self, number, initial=3, prefer_next=False): + next_num = self.find_next_multiple_of_power_two(number=number - 1, initial=initial) + previous_num = next_num >> 1 + return next_num if abs(next_num - number) < (abs(previous_num - number) + int(prefer_next)) else previous_num + + def create_chart(self, + figure_size=(41.8330013267, 16.7332005307), + subplot=111, + step_size=0.5, + bar_height=0.3, + version_alpha=0.8, + lts_service_color='darkred', + lts_maintenance_color='red', + bar_align='center', + date_interval=None, + output_chart_name='docs/chart', + output_chart_extension='.png', + months_surrounding_chart=4, + service_period_label='Service period (Recommended for new designs)', + maintenance_period_text='Maintenance period'): + fig = plt.figure(figsize=figure_size) + ax = fig.add_subplot(subplot) + + labels_count = len(self.sorted_releases_supported) + + pos = np.arange(step_size, labels_count * step_size + step_size, step_size) + + for release, i in zip(self.sorted_releases_supported, range(labels_count)): + start_date = release.start_date_matplotlib_format + end_of_service_date = release.end_service_date_matplotlib_format + + end_date = release.end_of_life_date_matplotlib_format + + ax.barh((i * step_size) + step_size, (end_of_service_date or end_date) - start_date, left=start_date, + height=bar_height, align=bar_align, + color=lts_service_color, + alpha=version_alpha, + edgecolor=lts_service_color) + if end_of_service_date is not None: + ax.barh((i * step_size) + step_size, end_date - end_of_service_date, left=end_of_service_date, + height=bar_height, align=bar_align, + color=lts_maintenance_color, alpha=version_alpha, edgecolor=lts_maintenance_color) + + ax.set_ylim(bottom=0, ymax=labels_count * step_size + step_size) + + max_ax_date = Version.add_months( + max(self.sorted_releases_supported, + key=lambda version: version.get_end_of_life_date().replace(tzinfo=None)).get_end_of_life_date(), + months_surrounding_chart + 1).replace(day=1) + + min_ax_date = Version.add_months( + min(self.sorted_releases_supported, + key=lambda version: version.get_start_date().replace(tzinfo=None)).get_start_date(), + -months_surrounding_chart).replace(day=1) + + x_ax_interval = date_interval or self.find_nearest_multiple_of_power_two( + self.months_timedelta(max_ax_date, min_ax_date) // 10) + + ax.set_xlim(xmin=min_ax_date, xmax=max_ax_date) + + ax.grid(color='g', linestyle=':') + ax.xaxis_date() + + rule = rrulewrapper(MONTHLY, interval=x_ax_interval) + loc = RRuleLocator(rule) + formatter = DateFormatter("%b %Y") + + ax.xaxis.set_major_locator(loc) + ax.xaxis.set_major_formatter(formatter) + x_labels = ax.get_xticklabels() + plt.ylabel('ESP-IDF Release', size=12) + + ax.invert_yaxis() + fig.autofmt_xdate() + + darkred_patch = mpatches.Patch(color=lts_service_color, label=service_period_label) + red_patch = mpatches.Patch(color=lts_maintenance_color, label=maintenance_period_text) + + plt.setp(plt.yticks(pos, map(lambda x: x.version_name, self.sorted_releases_supported))[1], rotation=0, + fontsize=10, family='Tahoma') + plt.setp(x_labels, rotation=30, fontsize=11, family='Tahoma') + plt.legend(handles=[darkred_patch, red_patch], prop={'size': 10, 'family': 'Tahoma'}, + bbox_to_anchor=(1.01, 1.165), loc='upper right') + fig.set_size_inches(11, 5, forward=True) + plt.savefig(output_chart_name + output_chart_extension, bbox_inches='tight') + print("Saved into " + output_chart_name + output_chart_extension) + + +if __name__ == '__main__': + arg_parser = argparse.ArgumentParser( + description="Create chart of version support. Set the url or filename with versions." + "If you set both filename and url the script will prefer filename.") + arg_parser.add_argument("--url", metavar="URL", default="https://dl.espressif.com/dl/esp-idf/idf_versions.js") + arg_parser.add_argument("--filename", + help="Set the name of the source file, if is set, the script ignores the url.") + arg_parser.add_argument("--output-format", help="Set the output format of the image.", default="svg") + arg_parser.add_argument("--output-file", help="Set the name of the output file.", default="docs/chart") + args = arg_parser.parse_args() + + ChartVersions(url=args.url if args.filename is None else None, filename=args.filename).create_chart( + output_chart_extension="." + args.output_format.lower()[-3:], output_chart_name=args.output_file) diff --git a/docs/requirements.txt b/docs/requirements.txt index 5b0b97b4c0..1a331e7c98 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,6 +1,10 @@ # This is a list of python packages used to generate documentation. This file is used with pip: # pip install --user -r requirements.txt # +# matplotlib is currently required only by the script generate_chart.py +matplotlib==3.3.1 ; python_version>="3" +matplotlib==2.0.1 ; python_version=="2.7" +cairosvg==2.4.2 # required by sphinxcontrib-svg2pdfconverter[CairoSVG] sphinx==2.3.1 breathe==4.14.1 sphinx-copybutton==0.3.0 @@ -10,6 +14,7 @@ sphinxcontrib-seqdiag==2.0.0 sphinxcontrib-actdiag==2.0.0 sphinxcontrib-nwdiag==2.0.0 sphinxcontrib-wavedrom==2.0.0 +sphinxcontrib-svg2pdfconverter[CairoSVG]==1.1.0 nwdiag==2.0.0 recommonmark future>=0.16.0 # for ../tools/gen_esp_err_to_name.py diff --git a/generate_chart.py b/generate_chart.py deleted file mode 100644 index d1f61743dc..0000000000 --- a/generate_chart.py +++ /dev/null @@ -1,265 +0,0 @@ -#!/usr/bin/env python -import datetime as dt -import requests -import json -import os -import re - -import matplotlib.dates -import matplotlib.font_manager as font_manager -import matplotlib.patches as mpatches -import matplotlib.pyplot as plt -import numpy as np -from dateutil import parser -from dateutil.relativedelta import relativedelta -from matplotlib.dates import WEEKLY, DateFormatter, RRuleLocator, rrulewrapper - - -class Version(object): - def __init__(self, version_name, - explicit_start_date=None, - explicit_end_date=None, - explicit_end_service_date=None, - is_lts=None): - self.version_name = version_name - - self.is_lts = is_lts or self.is_version_lts() - - self.is_major_minor = Version.is_minor_major_version(self.version_name) - - self._start_date = parser.parse( - explicit_start_date) if explicit_start_date is not None else self._retrieve_start_date() - self._end_of_life_date = parser.parse( - explicit_end_date) if explicit_end_date is not None else self._retrieve_end_of_life_date() - self._end_service_date = parser.parse( - explicit_end_service_date) if explicit_end_service_date is not None else self.get_end_service_date() - - self.start_date_matplotlib_format = matplotlib.dates.date2num(self._start_date) - self.end_of_life_date_matplotlib_format = matplotlib.dates.date2num(self._end_of_life_date) - - self.end_service_date_matplotlib_format = matplotlib.dates.date2num( - self._end_service_date) if self._end_service_date is not None else None - - @staticmethod - def get_config(config_path): - return json.load(open(config_path)) - - @staticmethod - def add_months(source_date, months): - return source_date + relativedelta(months=+months) - - @staticmethod - def is_minor_major_version(version_name): - return True if len(version_name.split(".")) <= 2 else False - - def is_version_lts(self): - version = self.version_name - return version >= 'v4.1' - - def get_start_date(self): - return self._start_date - - def get_end_of_life_date(self): - return self._end_of_life_date - - def _retrieve_start_date(self): - return parser.parse(os.popen("git log -1 --format=%ai " + self.version_name)) - - def _retrieve_end_of_life_date(self): - return self.add_months(self._start_date, 30 if self.is_lts else 18) - - def get_end_service_date(self): - return self.add_months(self._start_date, 12) - - -class ChartVersions(object): - def __init__(self): - self.all_versions = self.get_all_versions_from_git() - - self._releases = self._get_releases_from_url(url="https://dl.espressif.com/dl/esp-idf/idf_versions.js") - self._patches = self._get_patches_from_url(url="https://dl.espressif.com/dl/esp-idf/idf_versions.js") - - self.sorted_releases_supported = sorted(self.filter_old_versions(self._releases), key=lambda x: x.version_name) - self.patches_supported = self.filter_old_versions(self._patches) - - # TODO test higher versions - # self.sorted_releases_supported.append(Version(version_name='v4.1', explicit_start_date="28-9-2020")) - # self.sorted_releases_supported.append(Version(version_name='v4.2', explicit_start_date="28-5-2021")) - - def get_releases_as_json(self): - return { - x.version_name: { - "start_date": x.get_start_date().strftime("%Y-%m-%d"), - "end_date": x.get_end_of_life_date().strftime("%Y-%m-%d"), - "is_lts": x.is_lts - } for x in self.sorted_releases_supported + self.patches_supported - } - - @staticmethod - def parse_chart_releases_from_js(js_as_string): - return json.loads(js_as_string[js_as_string.find("RELEASES: ") + len("RELEASES: "):js_as_string.rfind("};")]) - - def _get_all_version_from_url(self, url=None, filename=None): - releases_file = requests.get(url).text if url is not None else "".join(open(filename).readlines()) - return self.parse_chart_releases_from_js(releases_file) - - def _get_releases_from_url(self, url=None, filename=None): - all_versions = self._get_all_version_from_url(url, filename) - return [ - Version(version_name=x, - explicit_start_date=all_versions[x]['start_date'], - explicit_end_date=all_versions[x]['end_date'] if 'end_date' in all_versions[x].keys() else None, - explicit_end_service_date=all_versions[x]['end_service'] if 'end_service' in all_versions[x].keys() else None) - for x in all_versions.keys() if Version.is_minor_major_version(x) - ] - - def _get_patches_from_url(self, url=None, filename=None): - all_versions = self._get_all_version_from_url(url, filename) - return [ - Version(version_name=x, - explicit_start_date=all_versions[x]['start_date'], - explicit_end_date=all_versions[x]['end_date'] if 'end_date' in all_versions[x].keys() else None, - is_lts=all_versions[x]['new_policy'] if 'new_policy' in all_versions[x].keys() else None) - for x in all_versions.keys() if not Version.is_minor_major_version(x) - ] - - def _get_releases(self, all_versions): - return [Version(release) for release in self._get_releases_names(all_versions)] - - def _get_patches(self, all_versions): - return [Version(patch, explicit_end_date=self.get_super_end_date_as_string(patch), - is_lts=self.get_super_lts_state(patch)) for patch in self._get_patches_names(all_versions)] - - @staticmethod - def _get_releases_names(all_versions): - return list(filter(lambda x: Version.is_minor_major_version(x), all_versions)) - - @staticmethod - def _get_patches_names(all_versions): - return list(filter(lambda x: not Version.is_minor_major_version(x), all_versions)) - - def get_super_version(self, patch): - return list(filter(lambda x: x.version_name == self.get_super_version_name(patch), self._releases))[0] - - def get_super_end_date_as_string(self, patch): - return self.get_super_version(patch).get_end_of_life_date().strftime("%m/%d/%Y, %H:%M:%S") - - def get_super_lts_state(self, patch): - return self.get_super_version(patch).is_lts - - @staticmethod - def get_all_versions_from_git(): - """ - :returns: list of string variables meaning name of the versions or patches (e.g. v4.1, v3.3.1, etc.) - """ - all_git_tags = os.popen("git tag") - stable_releases_regex = "(?:^|)(v\d+\.\d+(?:\.\d+){0,1})(?=\n|$)" - - results = [re.match(stable_releases_regex, line) for line in all_git_tags] - all_versions = [regex_result.group(1) for regex_result in results if regex_result is not None] - return all_versions - - @staticmethod - def filter_old_versions(versions): - return list( - filter(lambda x: x.get_end_of_life_date() >= dt.datetime.now(x.get_end_of_life_date().tzinfo), versions)) - - @staticmethod - def get_super_version_name(version_name): - return ".".join(version_name.split(".")[:2]) - - def create_chart(self, - figure_size=(20, 8), - subplot=111, - step_size=0.5, - bar_height=0.3, - version_alpha=0.8, - patch_width=1, - lts_service_color='darkred', - lts_maintenance_color='red', - bar_align='center', - patch_color='black', - patch_alpha=1, - date_interval=10, - output_chart_name='docs/chart', - output_chart_extension='.png', - months_surrounding_chart=5): - fig = plt.figure(figsize=figure_size) - ax = fig.add_subplot(subplot) - - labels_count = len(self.sorted_releases_supported) - - pos = np.arange(step_size, labels_count * step_size + step_size, step_size) - mapping_releases_index = {release.version_name: i for release, i in - zip(self.sorted_releases_supported, range(labels_count))} - - for release, i in zip(self.sorted_releases_supported, range(labels_count)): - start_date = release.start_date_matplotlib_format - end_of_service_date = release.end_service_date_matplotlib_format - - end_date = release.end_of_life_date_matplotlib_format - - ax.barh((i * step_size) + step_size, (end_of_service_date or end_date) - start_date, left=start_date, - height=bar_height, align=bar_align, - color=lts_service_color, - alpha=version_alpha, - edgecolor=lts_service_color) - if end_of_service_date is not None: - ax.barh((i * step_size) + step_size, end_date - end_of_service_date, left=end_of_service_date, - height=bar_height, align=bar_align, - color=lts_maintenance_color, alpha=version_alpha, edgecolor=lts_maintenance_color) - - for patch, i in zip(self.patches_supported, range(len(self.patches_supported))): - start_date = patch.start_date_matplotlib_format - - ax.barh(mapping_releases_index[self.get_super_version_name(patch.version_name)] * step_size + step_size, - patch_width, left=start_date, height=bar_height, align=bar_align, color=patch_color, - alpha=patch_alpha, - edgecolor=patch_color) - ax.text(start_date - 5, mapping_releases_index[ - self.get_super_version_name(patch.version_name)] * step_size + step_size - 0.2, - patch.version_name.split('.')[-1], - fontsize='xx-small', color='darkred') - - plt.setp(plt.yticks(pos, map(lambda x: x.version_name, self.sorted_releases_supported))[1], fontsize=14) - - ax.set_ylim(bottom=0, ymax=labels_count * step_size + step_size) - - ax.set_xlim( - xmin=Version.add_months( - min(self.sorted_releases_supported, - key=lambda version: version.get_start_date().replace(tzinfo=None)).get_start_date(), - -months_surrounding_chart), - xmax=Version.add_months( - max(self.sorted_releases_supported, - key=lambda version: version.get_end_of_life_date().replace(tzinfo=None)).get_end_of_life_date(), - months_surrounding_chart)) - - ax.grid(color='g', linestyle=':') - ax.xaxis_date() - - rule = rrulewrapper(WEEKLY, interval=date_interval) - loc = RRuleLocator(rule) - formatter = DateFormatter("%d-%b '%y") - - ax.xaxis.set_major_locator(loc) - ax.xaxis.set_major_formatter(formatter) - x_labels = ax.get_xticklabels() - plt.setp(x_labels, rotation=30, fontsize=10) - - ax.legend(loc=1, prop=font_manager.FontProperties(size='small')) - - ax.invert_yaxis() - fig.autofmt_xdate() - - darkred_patch = mpatches.Patch(color='darkred', label='Service period (Recommended for new designs)') - red_patch = mpatches.Patch(color='red', label='Maintenance period') - - plt.legend(handles=[darkred_patch, red_patch], prop={'size': 6}) - fig.set_size_inches(10, 5, forward=True) - plt.savefig(output_chart_name + output_chart_extension) - plt.show() - - -if __name__ == '__main__': - ChartVersions().create_chart() diff --git a/idf_versions.js b/idf_versions.js deleted file mode 100644 index d36ec3182c..0000000000 --- a/idf_versions.js +++ /dev/null @@ -1,96 +0,0 @@ -var DOCUMENTATION_VERSIONS = { - DEFAULTS: { - has_targets: false, - supported_targets: ["esp32"] - }, - VERSIONS: [{ - name: "latest", - has_targets: true, - supported_targets: ["esp32", "esp32s2"] - }, - { - name: "v4.1-rc", - pre_release: true - }, - { - name: "v4.0.1", - old: false - }, - { - name: "v4.0", - old: true - }, - { - name: "v3.3.2" - }, - { - name: "v3.3.1", - old: true - }, - { - name: "v3.3", - old: true - }, - { - name: "v3.2.3", - old: false - }, - { - name: "v3.1.7", - old: false - }, - { - name: "v3.1.6", - old: true - }, - { - name: "v3.0.9", - old: true - }, - { - name: "release-v4.2", - pre_release: true, - has_targets: true, - supported_targets: ["esp32", "esp32s2"] - }, - { - name: "release-v4.1", - pre_release: true - }, - { - name: "release-v4.0", - pre_release: true - }, - { - name: "release-v3.3", - pre_release: true - }, - { - name: "release-v3.2", - pre_release: true - }, - { - name: "release-v3.1", - pre_release: true - } - ], - RELEASES: { - "v3.1": { - "start_date": "2018-09-07", - "end_date": "2020-10-31" - }, - "v3.2": { - "start_date": "2019-04-11", - "end_date": "2020-10-31" - }, - "v3.3": { - "start_date": "2019-09-05", - "end_date": "2022-03-31" - }, - "v4.0": { - "start_date": "2020-02-11", - "end_service": "2021-02-11", - "end_date": "2021-10-31" - } - } -};