Refactor scripts arguments handling (#456)

- remove the use of environment variables to get directory paths,
- make use of arguments / argparse instead of environment variables in `update.py` and `report.py`,
- automatically guess the data directory in `latest.py` based on the script's location,
- propagate log level to auto scripts,
- move `list_configs_from_argv` from `endoflife` module to `releasedata` module,
- use `list_products` in `latest.py` to load the product's frontmatters.
This commit is contained in:
Marc Wrobel
2025-06-28 18:23:58 +02:00
parent 1dc08689f9
commit c78d1fe2b5
66 changed files with 273 additions and 256 deletions

View File

@@ -61,7 +61,7 @@ jobs:
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
continue-on-error: true # commit even if the data was not fully updated continue-on-error: true # commit even if the data was not fully updated
run: python update.py run: python update.py -p 'website/products'
- name: Commit changes - name: Commit changes
uses: stefanzweifel/git-auto-commit-action@v6 uses: stefanzweifel/git-auto-commit-action@v6

112
README.md
View File

@@ -20,7 +20,7 @@ Common Release Data for various projects in a consistent and easy-to-parse forma
## Currently Updated ## Currently Updated
As of 2025-05-17, 308 of the 379 products tracked by endoflife.date have automatically tracked releases: As of 2025-06-28, 316 of the 383 products tracked by endoflife.date have automatically tracked releases:
| Product | Permalink | Auto | Method(s) | | Product | Permalink | Auto | Method(s) |
|---------|-----------|------|-----------| |---------|-----------|------|-----------|
@@ -31,18 +31,18 @@ As of 2025-05-17, 308 of the 379 products tracked by endoflife.date have automat
| Alpine Linux | [`/alpine-linux`](https://endoflife.date/alpine-linux) | ✔️ | git, release_table | | Alpine Linux | [`/alpine-linux`](https://endoflife.date/alpine-linux) | ✔️ | git, release_table |
| Amazon CDK | [`/amazon-cdk`](https://endoflife.date/amazon-cdk) | ✔️ | git | | Amazon CDK | [`/amazon-cdk`](https://endoflife.date/amazon-cdk) | ✔️ | git |
| Amazon Corretto | [`/amazon-corretto`](https://endoflife.date/amazon-corretto) | ✔️ | github_releases | | Amazon Corretto | [`/amazon-corretto`](https://endoflife.date/amazon-corretto) | ✔️ | github_releases |
| Amazon EKS | [`/amazon-eks`](https://endoflife.date/amazon-eks) | ✔️ | custom, release_table | | Amazon EKS | [`/amazon-eks`](https://endoflife.date/amazon-eks) | ✔️ | amazon-eks, release_table |
| Amazon Glue | [`/amazon-glue`](https://endoflife.date/amazon-glue) | ❌ | | | Amazon Glue | [`/amazon-glue`](https://endoflife.date/amazon-glue) | ❌ | |
| Amazon Linux | [`/amazon-linux`](https://endoflife.date/amazon-linux) | ✔️ | docker_hub | | Amazon Linux | [`/amazon-linux`](https://endoflife.date/amazon-linux) | ✔️ | docker_hub |
| Amazon Neptune | [`/amazon-neptune`](https://endoflife.date/amazon-neptune) | ✔️ | custom, release_table | | Amazon Neptune | [`/amazon-neptune`](https://endoflife.date/amazon-neptune) | ✔️ | amazon-neptune, release_table |
| Amazon RDS for MariaDB | [`/amazon-rds-mariadb`](https://endoflife.date/amazon-rds-mariadb) | ✔️ | custom, release_table | | Amazon RDS for MariaDB | [`/amazon-rds-mariadb`](https://endoflife.date/amazon-rds-mariadb) | ✔️ | rds, release_table |
| Amazon RDS for MySQL | [`/amazon-rds-mysql`](https://endoflife.date/amazon-rds-mysql) | ✔️ | custom, release_table | | Amazon RDS for MySQL | [`/amazon-rds-mysql`](https://endoflife.date/amazon-rds-mysql) | ✔️ | rds, release_table |
| Amazon RDS for PostgreSQL | [`/amazon-rds-postgresql`](https://endoflife.date/amazon-rds-postgresql) | ✔️ | custom, release_table | | Amazon RDS for PostgreSQL | [`/amazon-rds-postgresql`](https://endoflife.date/amazon-rds-postgresql) | ✔️ | rds, release_table |
| Android OS | [`/android`](https://endoflife.date/android) | ❌ | | | Android OS | [`/android`](https://endoflife.date/android) | ❌ | |
| Angular | [`/angular`](https://endoflife.date/angular) | ✔️ | git, release_table | | Angular | [`/angular`](https://endoflife.date/angular) | ✔️ | git, release_table |
| AngularJS | [`/angularjs`](https://endoflife.date/angularjs) | ✔️ | npm | | AngularJS | [`/angularjs`](https://endoflife.date/angularjs) | ✔️ | npm |
| Ansible-core | [`/ansible-core`](https://endoflife.date/ansible-core) | ✔️ | git, release_table |
| Ansible | [`/ansible`](https://endoflife.date/ansible) | ✔️ | pypi | | Ansible | [`/ansible`](https://endoflife.date/ansible) | ✔️ | pypi |
| Ansible-core | [`/ansible-core`](https://endoflife.date/ansible-core) | ✔️ | git, release_table |
| antiX Linux | [`/antix`](https://endoflife.date/antix) | ✔️ | distrowatch | | antiX Linux | [`/antix`](https://endoflife.date/antix) | ✔️ | distrowatch |
| Apache ActiveMQ | [`/apache-activemq`](https://endoflife.date/apache-activemq) | ✔️ | git | | Apache ActiveMQ | [`/apache-activemq`](https://endoflife.date/apache-activemq) | ✔️ | git |
| Apache Airflow | [`/apache-airflow`](https://endoflife.date/apache-airflow) | ✔️ | pypi, release_table | | Apache Airflow | [`/apache-airflow`](https://endoflife.date/apache-airflow) | ✔️ | pypi, release_table |
@@ -55,7 +55,7 @@ As of 2025-05-17, 308 of the 379 products tracked by endoflife.date have automat
| Apache Groovy | [`/apache-groovy`](https://endoflife.date/apache-groovy) | ✔️ | maven | | Apache Groovy | [`/apache-groovy`](https://endoflife.date/apache-groovy) | ✔️ | maven |
| Apache Hadoop | [`/apache-hadoop`](https://endoflife.date/apache-hadoop) | ✔️ | git | | Apache Hadoop | [`/apache-hadoop`](https://endoflife.date/apache-hadoop) | ✔️ | git |
| Apache Hop | [`/apache-hop`](https://endoflife.date/apache-hop) | ✔️ | maven | | Apache Hop | [`/apache-hop`](https://endoflife.date/apache-hop) | ✔️ | maven |
| Apache HTTP Server | [`/apache-http-server`](https://endoflife.date/apache-http-server) | ✔️ | custom | | Apache HTTP Server | [`/apache-http-server`](https://endoflife.date/apache-http-server) | ✔️ | apache-http-server |
| Apache Kafka | [`/apache-kafka`](https://endoflife.date/apache-kafka) | ✔️ | git, release_table | | Apache Kafka | [`/apache-kafka`](https://endoflife.date/apache-kafka) | ✔️ | git, release_table |
| Apache Lucene | [`/apache-lucene`](https://endoflife.date/apache-lucene) | ✔️ | maven | | Apache Lucene | [`/apache-lucene`](https://endoflife.date/apache-lucene) | ✔️ | maven |
| Apache Maven | [`/apache-maven`](https://endoflife.date/apache-maven) | ✔️ | maven | | Apache Maven | [`/apache-maven`](https://endoflife.date/apache-maven) | ✔️ | maven |
@@ -63,14 +63,14 @@ As of 2025-05-17, 308 of the 379 products tracked by endoflife.date have automat
| Apache Pulsar | [`/apache-pulsar`](https://endoflife.date/apache-pulsar) | ✔️ | github_releases, release_table | | Apache Pulsar | [`/apache-pulsar`](https://endoflife.date/apache-pulsar) | ✔️ | github_releases, release_table |
| Apache Spark | [`/apache-spark`](https://endoflife.date/apache-spark) | ✔️ | git | | Apache Spark | [`/apache-spark`](https://endoflife.date/apache-spark) | ✔️ | git |
| Apache Struts | [`/apache-struts`](https://endoflife.date/apache-struts) | ✔️ | maven | | Apache Struts | [`/apache-struts`](https://endoflife.date/apache-struts) | ✔️ | maven |
| Apache Subversion | [`/apache-subversion`](https://endoflife.date/apache-subversion) | ✔️ | custom | | Apache Subversion | [`/apache-subversion`](https://endoflife.date/apache-subversion) | ✔️ | apache-subversion |
| API Platform | [`/api-platform`](https://endoflife.date/api-platform) | ✔️ | git | | API Platform | [`/api-platform`](https://endoflife.date/api-platform) | ✔️ | git |
| Apple tvOS | [`/tvos`](https://endoflife.date/tvos) | ✔️ | apple | | Apple tvOS | [`/tvos`](https://endoflife.date/tvos) | ✔️ | apple |
| Apple Watch | [`/apple-watch`](https://endoflife.date/apple-watch) | ❌ | | | Apple Watch | [`/apple-watch`](https://endoflife.date/apple-watch) | ❌ | |
| ArangoDB | [`/arangodb`](https://endoflife.date/arangodb) | ✔️ | git | | ArangoDB | [`/arangodb`](https://endoflife.date/arangodb) | ✔️ | git |
| Argo CD | [`/argo-cd`](https://endoflife.date/argo-cd) | ✔️ | git | | Argo CD | [`/argo-cd`](https://endoflife.date/argo-cd) | ✔️ | git |
| Artifactory | [`/artifactory`](https://endoflife.date/artifactory) | ✔️ | custom | | Artifactory | [`/artifactory`](https://endoflife.date/artifactory) | ✔️ | artifactory |
| AWS Lambda | [`/aws-lambda`](https://endoflife.date/aws-lambda) | ✔️ | custom | | AWS Lambda | [`/aws-lambda`](https://endoflife.date/aws-lambda) | ✔️ | aws-lambda |
| Azul Zulu | [`/azul-zulu`](https://endoflife.date/azul-zulu) | ❌ | | | Azul Zulu | [`/azul-zulu`](https://endoflife.date/azul-zulu) | ❌ | |
| Azure DevOps Server | [`/azure-devops-server`](https://endoflife.date/azure-devops-server) | ❌ | | | Azure DevOps Server | [`/azure-devops-server`](https://endoflife.date/azure-devops-server) | ❌ | |
| Azure Kubernetes Service | [`/azure-kubernetes-service`](https://endoflife.date/azure-kubernetes-service) | ❌ | | | Azure Kubernetes Service | [`/azure-kubernetes-service`](https://endoflife.date/azure-kubernetes-service) | ❌ | |
@@ -88,14 +88,14 @@ As of 2025-05-17, 308 of the 379 products tracked by endoflife.date have automat
| caddy | [`/caddy`](https://endoflife.date/caddy) | ✔️ | git | | caddy | [`/caddy`](https://endoflife.date/caddy) | ✔️ | git |
| CakePHP | [`/cakephp`](https://endoflife.date/cakephp) | ✔️ | git | | CakePHP | [`/cakephp`](https://endoflife.date/cakephp) | ✔️ | git |
| Calico | [`/calico`](https://endoflife.date/calico) | ✔️ | git | | Calico | [`/calico`](https://endoflife.date/calico) | ✔️ | git |
| CentOS Stream | [`/centos-stream`](https://endoflife.date/centos-stream) | ❌ | |
| CentOS | [`/centos`](https://endoflife.date/centos) | ❌ | | | CentOS | [`/centos`](https://endoflife.date/centos) | ❌ | |
| CentOS Stream | [`/centos-stream`](https://endoflife.date/centos-stream) | ❌ | |
| Centreon | [`/centreon`](https://endoflife.date/centreon) | ✔️ | git, release_table | | Centreon | [`/centreon`](https://endoflife.date/centreon) | ✔️ | git, release_table |
| cert-manager | [`/cert-manager`](https://endoflife.date/cert-manager) | ✔️ | git | | cert-manager | [`/cert-manager`](https://endoflife.date/cert-manager) | ✔️ | git |
| CFEngine | [`/cfengine`](https://endoflife.date/cfengine) | ✔️ | git | | CFEngine | [`/cfengine`](https://endoflife.date/cfengine) | ✔️ | git |
| Chef Infra Client | [`/chef-infra-client`](https://endoflife.date/chef-infra-client) | ✔️ | custom | | Chef Infra Client | [`/chef-infra-client`](https://endoflife.date/chef-infra-client) | ✔️ | chef-infra |
| Chef Infra Server | [`/chef-infra-server`](https://endoflife.date/chef-infra-server) | ✔️ | custom | | Chef Infra Server | [`/chef-infra-server`](https://endoflife.date/chef-infra-server) | ✔️ | chef-infra |
| Chef InSpec | [`/chef-inspec`](https://endoflife.date/chef-inspec) | ✔️ | custom | | Chef InSpec | [`/chef-inspec`](https://endoflife.date/chef-inspec) | ✔️ | chef-inspec |
| Citrix Virtual Apps and Desktops | [`/citrix-vad`](https://endoflife.date/citrix-vad) | ❌ | | | Citrix Virtual Apps and Desktops | [`/citrix-vad`](https://endoflife.date/citrix-vad) | ❌ | |
| CKEditor | [`/ckeditor`](https://endoflife.date/ckeditor) | ❌ | | | CKEditor | [`/ckeditor`](https://endoflife.date/ckeditor) | ❌ | |
| ClamAV | [`/clamav`](https://endoflife.date/clamav) | ✔️ | git | | ClamAV | [`/clamav`](https://endoflife.date/clamav) | ✔️ | git |
@@ -110,12 +110,12 @@ As of 2025-05-17, 308 of the 379 products tracked by endoflife.date have automat
| Contao | [`/contao`](https://endoflife.date/contao) | ✔️ | git | | Contao | [`/contao`](https://endoflife.date/contao) | ✔️ | git |
| Contour | [`/contour`](https://endoflife.date/contour) | ✔️ | git | | Contour | [`/contour`](https://endoflife.date/contour) | ✔️ | git |
| Control-M | [`/controlm`](https://endoflife.date/controlm) | ❌ | | | Control-M | [`/controlm`](https://endoflife.date/controlm) | ❌ | |
| Google Container-Optimized OS (COS) | [`/cos`](https://endoflife.date/cos) | ✔️ | custom | | Google Container-Optimized OS (COS) | [`/cos`](https://endoflife.date/cos) | ✔️ | cos |
| Couchbase Server | [`/couchbase-server`](https://endoflife.date/couchbase-server) | ✔️ | custom, release_table | | Couchbase Server | [`/couchbase-server`](https://endoflife.date/couchbase-server) | ✔️ | couchbase-server, release_table |
| Craft CMS | [`/craft-cms`](https://endoflife.date/craft-cms) | ✔️ | git, release_table | | Craft CMS | [`/craft-cms`](https://endoflife.date/craft-cms) | ✔️ | git, release_table |
| dbt Core | [`/dbt-core`](https://endoflife.date/dbt-core) | ✔️ | git | | dbt Core | [`/dbt-core`](https://endoflife.date/dbt-core) | ✔️ | git |
| DaoCloud Enterprise | [`/dce`](https://endoflife.date/dce) | ❌ | | | DaoCloud Enterprise | [`/dce`](https://endoflife.date/dce) | ❌ | |
| Debian | [`/debian`](https://endoflife.date/debian) | ✔️ | custom, release_table | | Debian | [`/debian`](https://endoflife.date/debian) | ✔️ | debian, release_table |
| Deno | [`/deno`](https://endoflife.date/deno) | ✔️ | git | | Deno | [`/deno`](https://endoflife.date/deno) | ✔️ | git |
| Dependency-Track | [`/dependency-track`](https://endoflife.date/dependency-track) | ✔️ | git | | Dependency-Track | [`/dependency-track`](https://endoflife.date/dependency-track) | ✔️ | git |
| Devuan | [`/devuan`](https://endoflife.date/devuan) | ✔️ | distrowatch | | Devuan | [`/devuan`](https://endoflife.date/devuan) | ✔️ | distrowatch |
@@ -142,7 +142,7 @@ As of 2025-05-17, 308 of the 379 products tracked by endoflife.date have automat
| Fedora Linux | [`/fedora`](https://endoflife.date/fedora) | ✔️ | distrowatch | | Fedora Linux | [`/fedora`](https://endoflife.date/fedora) | ✔️ | distrowatch |
| FFmpeg | [`/ffmpeg`](https://endoflife.date/ffmpeg) | ✔️ | git | | FFmpeg | [`/ffmpeg`](https://endoflife.date/ffmpeg) | ✔️ | git |
| FileMaker Platform | [`/filemaker`](https://endoflife.date/filemaker) | ✔️ | release_table | | FileMaker Platform | [`/filemaker`](https://endoflife.date/filemaker) | ✔️ | release_table |
| Firefox | [`/firefox`](https://endoflife.date/firefox) | ✔️ | custom | | Firefox | [`/firefox`](https://endoflife.date/firefox) | ✔️ | firefox |
| Fluent Bit | [`/fluent-bit`](https://endoflife.date/fluent-bit) | ✔️ | git | | Fluent Bit | [`/fluent-bit`](https://endoflife.date/fluent-bit) | ✔️ | git |
| Flux | [`/flux`](https://endoflife.date/flux) | ✔️ | git | | Flux | [`/flux`](https://endoflife.date/flux) | ✔️ | git |
| Forgejo | [`/forgejo`](https://endoflife.date/forgejo) | ✔️ | git, release_table | | Forgejo | [`/forgejo`](https://endoflife.date/forgejo) | ✔️ | git, release_table |
@@ -150,34 +150,34 @@ As of 2025-05-17, 308 of the 379 products tracked by endoflife.date have automat
| FreeBSD | [`/freebsd`](https://endoflife.date/freebsd) | ❌ | | | FreeBSD | [`/freebsd`](https://endoflife.date/freebsd) | ❌ | |
| Gatekeeper | [`/gatekeeper`](https://endoflife.date/gatekeeper) | ✔️ | git | | Gatekeeper | [`/gatekeeper`](https://endoflife.date/gatekeeper) | ✔️ | git |
| Gerrit | [`/gerrit`](https://endoflife.date/gerrit) | ✔️ | git | | Gerrit | [`/gerrit`](https://endoflife.date/gerrit) | ✔️ | git |
| Glasgow Haskell Compiler (GHC) | [`/ghc`](https://endoflife.date/ghc) | ✔️ | custom, git | | Glasgow Haskell Compiler (GHC) | [`/ghc`](https://endoflife.date/ghc) | ✔️ | git |
| GitLab | [`/gitlab`](https://endoflife.date/gitlab) | ✔️ | git, release_table | | GitLab | [`/gitlab`](https://endoflife.date/gitlab) | ✔️ | git, release_table |
| Go | [`/go`](https://endoflife.date/go) | ✔️ | git | | Go | [`/go`](https://endoflife.date/go) | ✔️ | git |
| GoAccess | [`/goaccess`](https://endoflife.date/goaccess) | ✔️ | git | | GoAccess | [`/goaccess`](https://endoflife.date/goaccess) | ✔️ | git |
| Godot | [`/godot`](https://endoflife.date/godot) | ✔️ | git | | Godot | [`/godot`](https://endoflife.date/godot) | ✔️ | git |
| Google Kubernetes Engine | [`/google-kubernetes-engine`](https://endoflife.date/google-kubernetes-engine) | ✔️ | custom | | Google Kubernetes Engine | [`/google-kubernetes-engine`](https://endoflife.date/google-kubernetes-engine) | ✔️ | google-kubernetes-engine |
| Google Nexus | [`/google-nexus`](https://endoflife.date/google-nexus) | ❌ | | | Google Nexus | [`/google-nexus`](https://endoflife.date/google-nexus) | ❌ | |
| Gorilla Toolkit | [`/gorilla`](https://endoflife.date/gorilla) | ❌ | | | Gorilla Toolkit | [`/gorilla`](https://endoflife.date/gorilla) | ❌ | |
| GraalVM | [`/graalvm`](https://endoflife.date/graalvm) | ✔️ | custom | | GraalVM Community Edition | [`/graalvm-ce`](https://endoflife.date/graalvm-ce) | ✔️ | graalvm |
| Gradle | [`/gradle`](https://endoflife.date/gradle) | ✔️ | git | | Gradle | [`/gradle`](https://endoflife.date/gradle) | ✔️ | git |
| Grafana Loki | [`/grafana-loki`](https://endoflife.date/grafana-loki) | ✔️ | git |
| Grafana | [`/grafana`](https://endoflife.date/grafana) | ✔️ | github_releases, release_table | | Grafana | [`/grafana`](https://endoflife.date/grafana) | ✔️ | github_releases, release_table |
| Grafana Loki | [`/grafana-loki`](https://endoflife.date/grafana-loki) | ✔️ | git |
| Grails Framework | [`/grails`](https://endoflife.date/grails) | ✔️ | git | | Grails Framework | [`/grails`](https://endoflife.date/grails) | ✔️ | git |
| Graylog | [`/graylog`](https://endoflife.date/graylog) | ✔️ | git | | Graylog | [`/graylog`](https://endoflife.date/graylog) | ✔️ | git |
| Greenlight | [`/greenlight`](https://endoflife.date/greenlight) | ✔️ | git | | Greenlight | [`/greenlight`](https://endoflife.date/greenlight) | ✔️ | git |
| Grunt | [`/grunt`](https://endoflife.date/grunt) | ✔️ | git | | Grunt | [`/grunt`](https://endoflife.date/grunt) | ✔️ | git |
| GStreamer | [`/gstreamer`](https://endoflife.date/gstreamer) | ✔️ | git | | GStreamer | [`/gstreamer`](https://endoflife.date/gstreamer) | ✔️ | git |
| Guzzle | [`/guzzle`](https://endoflife.date/guzzle) | ✔️ | git | | Guzzle | [`/guzzle`](https://endoflife.date/guzzle) | ✔️ | git |
| HAProxy | [`/haproxy`](https://endoflife.date/haproxy) | ✔️ | custom | | HAProxy | [`/haproxy`](https://endoflife.date/haproxy) | ✔️ | haproxy |
| Harbor | [`/harbor`](https://endoflife.date/harbor) | ✔️ | git | | Harbor | [`/harbor`](https://endoflife.date/harbor) | ✔️ | git |
| Hashicorp Packer | [`/hashicorp-packer`](https://endoflife.date/hashicorp-packer) | ✔️ | git | | Hashicorp Packer | [`/hashicorp-packer`](https://endoflife.date/hashicorp-packer) | ✔️ | git |
| Hashicorp Vault | [`/hashicorp-vault`](https://endoflife.date/hashicorp-vault) | ✔️ | git | | Hashicorp Vault | [`/hashicorp-vault`](https://endoflife.date/hashicorp-vault) | ✔️ | git |
| Apache HBase | [`/hbase`](https://endoflife.date/hbase) | ✔️ | git | | Apache HBase | [`/hbase`](https://endoflife.date/hbase) | ✔️ | git |
| IBM AIX | [`/ibm-aix`](https://endoflife.date/ibm-aix) | ✔️ | custom, release_table | | IBM AIX | [`/ibm-aix`](https://endoflife.date/ibm-aix) | ✔️ | ibm-aix, release_table |
| IBM iSeries | [`/ibm-i`](https://endoflife.date/ibm-i) | ✔️ | release_table | | IBM iSeries | [`/ibm-i`](https://endoflife.date/ibm-i) | ✔️ | release_table |
| IBM Semeru Runtime | [`/ibm-semeru-runtime`](https://endoflife.date/ibm-semeru-runtime) | ✔️ | github_releases, release_table | | IBM Semeru Runtime | [`/ibm-semeru-runtime`](https://endoflife.date/ibm-semeru-runtime) | ✔️ | github_releases, release_table |
| Icinga Web | [`/icinga-web`](https://endoflife.date/icinga-web) | ✔️ | git |
| Icinga | [`/icinga`](https://endoflife.date/icinga) | ✔️ | git | | Icinga | [`/icinga`](https://endoflife.date/icinga) | ✔️ | git |
| Icinga Web | [`/icinga-web`](https://endoflife.date/icinga-web) | ✔️ | git |
| Intel Processors | [`/intel-processors`](https://endoflife.date/intel-processors) | ❌ | | | Intel Processors | [`/intel-processors`](https://endoflife.date/intel-processors) | ❌ | |
| Internet Explorer | [`/internet-explorer`](https://endoflife.date/internet-explorer) | ❌ | | | Internet Explorer | [`/internet-explorer`](https://endoflife.date/internet-explorer) | ❌ | |
| Ionic Framework | [`/ionic`](https://endoflife.date/ionic) | ✔️ | git, release_table | | Ionic Framework | [`/ionic`](https://endoflife.date/ionic) | ✔️ | git, release_table |
@@ -192,8 +192,8 @@ As of 2025-05-17, 308 of the 379 products tracked by endoflife.date have automat
| JHipster | [`/jhipster`](https://endoflife.date/jhipster) | ✔️ | npm | | JHipster | [`/jhipster`](https://endoflife.date/jhipster) | ✔️ | npm |
| Jira Software | [`/jira-software`](https://endoflife.date/jira-software) | ✔️ | atlassian_eol, atlassian_versions | | Jira Software | [`/jira-software`](https://endoflife.date/jira-software) | ✔️ | atlassian_eol, atlassian_versions |
| Joomla! | [`/joomla`](https://endoflife.date/joomla) | ✔️ | git | | Joomla! | [`/joomla`](https://endoflife.date/joomla) | ✔️ | git |
| jQuery UI | [`/jquery-ui`](https://endoflife.date/jquery-ui) | ✔️ | git |
| jQuery | [`/jquery`](https://endoflife.date/jquery) | ✔️ | git | | jQuery | [`/jquery`](https://endoflife.date/jquery) | ✔️ | git |
| jQuery UI | [`/jquery-ui`](https://endoflife.date/jquery-ui) | ✔️ | git |
| JReleaser | [`/jreleaser`](https://endoflife.date/jreleaser) | ✔️ | maven | | JReleaser | [`/jreleaser`](https://endoflife.date/jreleaser) | ✔️ | maven |
| Julia | [`/julia`](https://endoflife.date/julia) | ✔️ | git | | Julia | [`/julia`](https://endoflife.date/julia) | ✔️ | git |
| KDE Plasma | [`/kde-plasma`](https://endoflife.date/kde-plasma) | ✔️ | git | | KDE Plasma | [`/kde-plasma`](https://endoflife.date/kde-plasma) | ✔️ | git |
@@ -204,21 +204,22 @@ As of 2025-05-17, 308 of the 379 products tracked by endoflife.date have automat
| Kirby | [`/kirby`](https://endoflife.date/kirby) | ✔️ | git | | Kirby | [`/kirby`](https://endoflife.date/kirby) | ✔️ | git |
| Kong Gateway | [`/kong-gateway`](https://endoflife.date/kong-gateway) | ✔️ | git | | Kong Gateway | [`/kong-gateway`](https://endoflife.date/kong-gateway) | ✔️ | git |
| Kotlin | [`/kotlin`](https://endoflife.date/kotlin) | ✔️ | github_releases | | Kotlin | [`/kotlin`](https://endoflife.date/kotlin) | ✔️ | github_releases |
| Kubernetes | [`/kubernetes`](https://endoflife.date/kubernetes) | ✔️ | git |
| Kubernetes CSI Node Driver Registrar | [`/kubernetes-csi-node-driver-registrar`](https://endoflife.date/kubernetes-csi-node-driver-registrar) | ✔️ | git | | Kubernetes CSI Node Driver Registrar | [`/kubernetes-csi-node-driver-registrar`](https://endoflife.date/kubernetes-csi-node-driver-registrar) | ✔️ | git |
| Kubernetes Node Feature Discovery | [`/kubernetes-node-feature-discovery`](https://endoflife.date/kubernetes-node-feature-discovery) | ✔️ | github_releases | | Kubernetes Node Feature Discovery | [`/kubernetes-node-feature-discovery`](https://endoflife.date/kubernetes-node-feature-discovery) | ✔️ | github_releases |
| Kubernetes | [`/kubernetes`](https://endoflife.date/kubernetes) | ✔️ | git | | Kuma | [`/kuma`](https://endoflife.date/kuma) | ✔️ | git, kuma |
| Kuma | [`/kuma`](https://endoflife.date/kuma) | ✔️ | git |
| Kyverno | [`/kyverno`](https://endoflife.date/kyverno) | ✔️ | git | | Kyverno | [`/kyverno`](https://endoflife.date/kyverno) | ✔️ | git |
| Laravel | [`/laravel`](https://endoflife.date/laravel) | ✔️ | git, release_table | | Laravel | [`/laravel`](https://endoflife.date/laravel) | ✔️ | git, release_table |
| LDAP Account Manager | [`/ldap-account-manager`](https://endoflife.date/ldap-account-manager) | ✔️ | git | | LDAP Account Manager | [`/ldap-account-manager`](https://endoflife.date/ldap-account-manager) | ✔️ | git |
| LibreOffice | [`/libreoffice`](https://endoflife.date/libreoffice) | ✔️ | custom | | LibreOffice | [`/libreoffice`](https://endoflife.date/libreoffice) | ✔️ | libreoffice |
| LineageOS | [`/lineageos`](https://endoflife.date/lineageos) | ❌ | | | LineageOS | [`/lineageos`](https://endoflife.date/lineageos) | ❌ | |
| Linux Kernel | [`/linux`](https://endoflife.date/linux) | ✔️ | github_tags | | Linux Kernel | [`/linux`](https://endoflife.date/linux) | ✔️ | github_tags |
| Linux Mint | [`/linuxmint`](https://endoflife.date/linuxmint) | ✔️ | release_table | | Linux Mint | [`/linuxmint`](https://endoflife.date/linuxmint) | ✔️ | release_table |
| Liquibase | [`/liquibase`](https://endoflife.date/liquibase) | ✔️ | maven |
| Apache Log4j | [`/log4j`](https://endoflife.date/log4j) | ✔️ | maven | | Apache Log4j | [`/log4j`](https://endoflife.date/log4j) | ✔️ | maven |
| Logstash | [`/logstash`](https://endoflife.date/logstash) | ✔️ | git | | Logstash | [`/logstash`](https://endoflife.date/logstash) | ✔️ | git |
| Looker | [`/looker`](https://endoflife.date/looker) | ✔️ | custom | | Looker | [`/looker`](https://endoflife.date/looker) | ✔️ | looker |
| Lua | [`/lua`](https://endoflife.date/lua) | ✔️ | custom | | Lua | [`/lua`](https://endoflife.date/lua) | ✔️ | lua |
| Apple macOS | [`/macos`](https://endoflife.date/macos) | ✔️ | apple | | Apple macOS | [`/macos`](https://endoflife.date/macos) | ✔️ | apple |
| Mageia | [`/mageia`](https://endoflife.date/mageia) | ✔️ | distrowatch | | Mageia | [`/mageia`](https://endoflife.date/mageia) | ✔️ | distrowatch |
| Magento | [`/magento`](https://endoflife.date/magento) | ✔️ | git | | Magento | [`/magento`](https://endoflife.date/magento) | ✔️ | git |
@@ -244,7 +245,7 @@ As of 2025-05-17, 308 of the 379 products tracked by endoflife.date have automat
| Neo4j | [`/neo4j`](https://endoflife.date/neo4j) | ✔️ | git, release_table | | Neo4j | [`/neo4j`](https://endoflife.date/neo4j) | ✔️ | git, release_table |
| Neos | [`/neos`](https://endoflife.date/neos) | ✔️ | git | | Neos | [`/neos`](https://endoflife.date/neos) | ✔️ | git |
| NetApp ONTAP | [`/netapp-ontap`](https://endoflife.date/netapp-ontap) | ❌ | | | NetApp ONTAP | [`/netapp-ontap`](https://endoflife.date/netapp-ontap) | ❌ | |
| NetBSD | [`/netbsd`](https://endoflife.date/netbsd) | ✔️ | custom | | NetBSD | [`/netbsd`](https://endoflife.date/netbsd) | ✔️ | netbsd |
| Nextcloud | [`/nextcloud`](https://endoflife.date/nextcloud) | ✔️ | git, release_table | | Nextcloud | [`/nextcloud`](https://endoflife.date/nextcloud) | ✔️ | git, release_table |
| Next.js | [`/nextjs`](https://endoflife.date/nextjs) | ✔️ | npm | | Next.js | [`/nextjs`](https://endoflife.date/nextjs) | ✔️ | npm |
| Nexus Repository | [`/nexus`](https://endoflife.date/nexus) | ✔️ | git, release_table | | Nexus Repository | [`/nexus`](https://endoflife.date/nexus) | ✔️ | git, release_table |
@@ -254,6 +255,7 @@ As of 2025-05-17, 308 of the 379 products tracked by endoflife.date have automat
| Node.js | [`/nodejs`](https://endoflife.date/nodejs) | ✔️ | git | | Node.js | [`/nodejs`](https://endoflife.date/nodejs) | ✔️ | git |
| Nokia Mobile | [`/nokia`](https://endoflife.date/nokia) | ❌ | | | Nokia Mobile | [`/nokia`](https://endoflife.date/nokia) | ❌ | |
| Nomad | [`/nomad`](https://endoflife.date/nomad) | ✔️ | git | | Nomad | [`/nomad`](https://endoflife.date/nomad) | ✔️ | git |
| Notepad++ | [`/notepad-plus-plus`](https://endoflife.date/notepad-plus-plus) | ✔️ | git |
| NumPy | [`/numpy`](https://endoflife.date/numpy) | ✔️ | pypi | | NumPy | [`/numpy`](https://endoflife.date/numpy) | ✔️ | pypi |
| Nutanix AOS | [`/nutanix-aos`](https://endoflife.date/nutanix-aos) | ✔️ | nutanix | | Nutanix AOS | [`/nutanix-aos`](https://endoflife.date/nutanix-aos) | ✔️ | nutanix |
| Nutanix Files | [`/nutanix-files`](https://endoflife.date/nutanix-files) | ✔️ | nutanix | | Nutanix Files | [`/nutanix-files`](https://endoflife.date/nutanix-files) | ✔️ | nutanix |
@@ -277,7 +279,8 @@ As of 2025-05-17, 308 of the 379 products tracked by endoflife.date have automat
| OPNsense | [`/opnsense`](https://endoflife.date/opnsense) | ✔️ | git | | OPNsense | [`/opnsense`](https://endoflife.date/opnsense) | ✔️ | git |
| Oracle APEX | [`/oracle-apex`](https://endoflife.date/oracle-apex) | ✔️ | release_table | | Oracle APEX | [`/oracle-apex`](https://endoflife.date/oracle-apex) | ✔️ | release_table |
| Oracle Database | [`/oracle-database`](https://endoflife.date/oracle-database) | ✔️ | release_table | | Oracle Database | [`/oracle-database`](https://endoflife.date/oracle-database) | ✔️ | release_table |
| Oracle JDK | [`/oracle-jdk`](https://endoflife.date/oracle-jdk) | ✔️ | custom, release_table | | Oracle GraalVM | [`/oracle-graalvm`](https://endoflife.date/oracle-graalvm) | ✔️ | graalvm, release_table |
| Oracle JDK | [`/oracle-jdk`](https://endoflife.date/oracle-jdk) | ✔️ | oracle-jdk, release_table |
| Oracle Linux | [`/oracle-linux`](https://endoflife.date/oracle-linux) | ✔️ | distrowatch | | Oracle Linux | [`/oracle-linux`](https://endoflife.date/oracle-linux) | ✔️ | distrowatch |
| Oracle Solaris | [`/oracle-solaris`](https://endoflife.date/oracle-solaris) | ❌ | | | Oracle Solaris | [`/oracle-solaris`](https://endoflife.date/oracle-solaris) | ❌ | |
| oVirt | [`/ovirt`](https://endoflife.date/ovirt) | ✔️ | git | | oVirt | [`/ovirt`](https://endoflife.date/ovirt) | ✔️ | git |
@@ -286,12 +289,12 @@ As of 2025-05-17, 308 of the 379 products tracked by endoflife.date have automat
| Palo Alto Networks PAN-OS | [`/panos`](https://endoflife.date/panos) | ✔️ | release_table | | Palo Alto Networks PAN-OS | [`/panos`](https://endoflife.date/panos) | ✔️ | release_table |
| PCI-DSS | [`/pci-dss`](https://endoflife.date/pci-dss) | ❌ | | | PCI-DSS | [`/pci-dss`](https://endoflife.date/pci-dss) | ❌ | |
| Perl | [`/perl`](https://endoflife.date/perl) | ✔️ | git | | Perl | [`/perl`](https://endoflife.date/perl) | ✔️ | git |
| PHP | [`/php`](https://endoflife.date/php) | ✔️ | custom | | PHP | [`/php`](https://endoflife.date/php) | ✔️ | php |
| phpBB | [`/phpbb`](https://endoflife.date/phpbb) | ✔️ | git | | phpBB | [`/phpbb`](https://endoflife.date/phpbb) | ✔️ | git |
| phpMyAdmin | [`/phpmyadmin`](https://endoflife.date/phpmyadmin) | ✔️ | git | | phpMyAdmin | [`/phpmyadmin`](https://endoflife.date/phpmyadmin) | ✔️ | git |
| Google Pixel Watch | [`/pixel-watch`](https://endoflife.date/pixel-watch) | ❌ | |
| Google Pixel | [`/pixel`](https://endoflife.date/pixel) | ❌ | | | Google Pixel | [`/pixel`](https://endoflife.date/pixel) | ❌ | |
| Plesk | [`/plesk`](https://endoflife.date/plesk) | ✔️ | custom | | Google Pixel Watch | [`/pixel-watch`](https://endoflife.date/pixel-watch) | | |
| Plesk | [`/plesk`](https://endoflife.date/plesk) | ✔️ | plesk |
| Plone | [`/plone`](https://endoflife.date/plone) | ✔️ | git, release_table | | Plone | [`/plone`](https://endoflife.date/plone) | ✔️ | git, release_table |
| pnpm | [`/pnpm`](https://endoflife.date/pnpm) | ✔️ | npm | | pnpm | [`/pnpm`](https://endoflife.date/pnpm) | ✔️ | npm |
| Podman | [`/podman`](https://endoflife.date/podman) | ✔️ | git | | Podman | [`/podman`](https://endoflife.date/podman) | ✔️ | git |
@@ -313,39 +316,40 @@ As of 2025-05-17, 308 of the 379 products tracked by endoflife.date have automat
| RabbitMQ | [`/rabbitmq`](https://endoflife.date/rabbitmq) | ✔️ | git | | RabbitMQ | [`/rabbitmq`](https://endoflife.date/rabbitmq) | ✔️ | git |
| Rancher | [`/rancher`](https://endoflife.date/rancher) | ✔️ | git | | Rancher | [`/rancher`](https://endoflife.date/rancher) | ✔️ | git |
| Raspberry Pi | [`/raspberry-pi`](https://endoflife.date/raspberry-pi) | ❌ | | | Raspberry Pi | [`/raspberry-pi`](https://endoflife.date/raspberry-pi) | ❌ | |
| React Native | [`/react-native`](https://endoflife.date/react-native) | ✔️ | npm |
| React | [`/react`](https://endoflife.date/react) | ✔️ | npm | | React | [`/react`](https://endoflife.date/react) | ✔️ | npm |
| React Native | [`/react-native`](https://endoflife.date/react-native) | ✔️ | npm |
| Red Hat build of OpenJDK | [`/redhat-build-of-openjdk`](https://endoflife.date/redhat-build-of-openjdk) | ✔️ | redhat_lifecycles | | Red Hat build of OpenJDK | [`/redhat-build-of-openjdk`](https://endoflife.date/redhat-build-of-openjdk) | ✔️ | redhat_lifecycles |
| Red Hat JBoss Enterprise Application Platform | [`/redhat-jboss-eap`](https://endoflife.date/redhat-jboss-eap) | ✔️ | custom, redhat_lifecycles | | Red Hat JBoss Enterprise Application Platform | [`/redhat-jboss-eap`](https://endoflife.date/redhat-jboss-eap) | ✔️ | red-hat-jboss-eap-7, red-hat-jboss-eap-8, redhat_lifecycles |
| Red Hat OpenShift | [`/red-hat-openshift`](https://endoflife.date/red-hat-openshift) | ✔️ | custom | | Red Hat OpenShift | [`/red-hat-openshift`](https://endoflife.date/red-hat-openshift) | ✔️ | red-hat-openshift |
| Red Hat Satellite | [`/redhat-satellite`](https://endoflife.date/redhat-satellite) | ✔️ | custom | | Red Hat Satellite | [`/redhat-satellite`](https://endoflife.date/redhat-satellite) | ✔️ | red-hat-satellite |
| Redis | [`/redis`](https://endoflife.date/redis) | ✔️ | git, release_table | | Redis | [`/redis`](https://endoflife.date/redis) | ✔️ | git, release_table |
| Redmine | [`/redmine`](https://endoflife.date/redmine) | ✔️ | git | | Redmine | [`/redmine`](https://endoflife.date/redmine) | ✔️ | git |
| Red Hat Enterprise Linux | [`/rhel`](https://endoflife.date/rhel) | ✔️ | redhat_lifecycles | | Red Hat Enterprise Linux | [`/rhel`](https://endoflife.date/rhel) | ✔️ | redhat_lifecycles |
| Robo | [`/robo`](https://endoflife.date/robo) | ✔️ | git, release_table | | Robo | [`/robo`](https://endoflife.date/robo) | ✔️ | git, release_table |
| Rocket.Chat | [`/rocket-chat`](https://endoflife.date/rocket-chat) | ✔️ | git | | Rocket.Chat | [`/rocket-chat`](https://endoflife.date/rocket-chat) | ✔️ | git |
| Rocky Linux | [`/rocky-linux`](https://endoflife.date/rocky-linux) | ✔️ | custom, release_table | | Rocky Linux | [`/rocky-linux`](https://endoflife.date/rocky-linux) | ✔️ | release_table, rocky-linux |
| ROS | [`/ros`](https://endoflife.date/ros) | ✔️ | ros |
| ROS 2 | [`/ros-2`](https://endoflife.date/ros-2) | ❌ | | | ROS 2 | [`/ros-2`](https://endoflife.date/ros-2) | ❌ | |
| ROS | [`/ros`](https://endoflife.date/ros) | ❌ | |
| Roundcube Webmail | [`/roundcube`](https://endoflife.date/roundcube) | ✔️ | git | | Roundcube Webmail | [`/roundcube`](https://endoflife.date/roundcube) | ✔️ | git |
| rtpengine | [`/rtpengine`](https://endoflife.date/rtpengine) | ✔️ | git | | rtpengine | [`/rtpengine`](https://endoflife.date/rtpengine) | ✔️ | git |
| Ruby on Rails | [`/rails`](https://endoflife.date/rails) | ✔️ | git |
| Ruby | [`/ruby`](https://endoflife.date/ruby) | ✔️ | git | | Ruby | [`/ruby`](https://endoflife.date/ruby) | ✔️ | git |
| Ruby on Rails | [`/rails`](https://endoflife.date/rails) | ✔️ | git |
| Rust | [`/rust`](https://endoflife.date/rust) | ✔️ | git | | Rust | [`/rust`](https://endoflife.date/rust) | ✔️ | git |
| Salt | [`/salt`](https://endoflife.date/salt) | ✔️ | git, release_table | | Salt | [`/salt`](https://endoflife.date/salt) | ✔️ | git, release_table |
| Samsung Galaxy Tab | [`/samsung-galaxy-tab`](https://endoflife.date/samsung-galaxy-tab) | ✔️ | samsung-security |
| Samsung Galaxy Watch | [`/samsung-galaxy-watch`](https://endoflife.date/samsung-galaxy-watch) | ❌ | | | Samsung Galaxy Watch | [`/samsung-galaxy-watch`](https://endoflife.date/samsung-galaxy-watch) | ❌ | |
| Samsung Mobile | [`/samsung-mobile`](https://endoflife.date/samsung-mobile) | ✔️ | custom | | Samsung Mobile | [`/samsung-mobile`](https://endoflife.date/samsung-mobile) | ✔️ | samsung-security |
| SapMachine | [`/sapmachine`](https://endoflife.date/sapmachine) | ✔️ | github_releases | | SapMachine | [`/sapmachine`](https://endoflife.date/sapmachine) | ✔️ | github_releases |
| Scala | [`/scala`](https://endoflife.date/scala) | ✔️ | github_releases | | Scala | [`/scala`](https://endoflife.date/scala) | ✔️ | github_releases |
| Microsoft SharePoint | [`/sharepoint`](https://endoflife.date/sharepoint) | ❌ | | | Microsoft SharePoint | [`/sharepoint`](https://endoflife.date/sharepoint) | ❌ | |
| Shopware | [`/shopware`](https://endoflife.date/shopware) | ✔️ | git | | Shopware | [`/shopware`](https://endoflife.date/shopware) | ✔️ | git |
| Silverstripe CMS | [`/silverstripe`](https://endoflife.date/silverstripe) | ✔️ | git, release_table | | Silverstripe CMS | [`/silverstripe`](https://endoflife.date/silverstripe) | ✔️ | git, release_table |
| Slackware Linux | [`/slackware`](https://endoflife.date/slackware) | ✔️ | distrowatch | | Slackware Linux | [`/slackware`](https://endoflife.date/slackware) | ✔️ | distrowatch |
| SUSE Linux Enterprise Server | [`/sles`](https://endoflife.date/sles) | | | | SUSE Linux Enterprise Server | [`/sles`](https://endoflife.date/sles) | ✔️ | sles |
| Apache Solr | [`/solr`](https://endoflife.date/solr) | ✔️ | git | | Apache Solr | [`/solr`](https://endoflife.date/solr) | ✔️ | git |
| SonarQube | [`/sonar`](https://endoflife.date/sonar) | ✔️ | git | | SonarQube | [`/sonar`](https://endoflife.date/sonar) | ✔️ | git |
| Sourcegraph | [`/sourcegraph`](https://endoflife.date/sourcegraph) | ✔️ | git | | Sourcegraph | [`/sourcegraph`](https://endoflife.date/sourcegraph) | ✔️ | git |
| Splunk | [`/splunk`](https://endoflife.date/splunk) | ✔️ | custom | | Splunk | [`/splunk`](https://endoflife.date/splunk) | ✔️ | splunk |
| Spring Boot | [`/spring-boot`](https://endoflife.date/spring-boot) | ✔️ | git, release_table | | Spring Boot | [`/spring-boot`](https://endoflife.date/spring-boot) | ✔️ | git, release_table |
| Spring Framework | [`/spring-framework`](https://endoflife.date/spring-framework) | ✔️ | git, release_table | | Spring Framework | [`/spring-framework`](https://endoflife.date/spring-framework) | ✔️ | git, release_table |
| SQLite | [`/sqlite`](https://endoflife.date/sqlite) | ✔️ | git | | SQLite | [`/sqlite`](https://endoflife.date/sqlite) | ✔️ | git |
@@ -362,7 +366,7 @@ As of 2025-05-17, 308 of the 379 products tracked by endoflife.date have automat
| Apache Tomcat | [`/tomcat`](https://endoflife.date/tomcat) | ✔️ | maven | | Apache Tomcat | [`/tomcat`](https://endoflife.date/tomcat) | ✔️ | maven |
| Traefik | [`/traefik`](https://endoflife.date/traefik) | ✔️ | git, release_table | | Traefik | [`/traefik`](https://endoflife.date/traefik) | ✔️ | git, release_table |
| Twig | [`/twig`](https://endoflife.date/twig) | ✔️ | git | | Twig | [`/twig`](https://endoflife.date/twig) | ✔️ | git |
| TYPO3 | [`/typo3`](https://endoflife.date/typo3) | ✔️ | custom | | TYPO3 | [`/typo3`](https://endoflife.date/typo3) | ✔️ | typo3 |
| Ubuntu | [`/ubuntu`](https://endoflife.date/ubuntu) | ✔️ | distrowatch | | Ubuntu | [`/ubuntu`](https://endoflife.date/ubuntu) | ✔️ | distrowatch |
| Umbraco CMS | [`/umbraco`](https://endoflife.date/umbraco) | ✔️ | git, release_table | | Umbraco CMS | [`/umbraco`](https://endoflife.date/umbraco) | ✔️ | git, release_table |
| Unity | [`/unity`](https://endoflife.date/unity) | ❌ | | | Unity | [`/unity`](https://endoflife.date/unity) | ❌ | |
@@ -372,10 +376,10 @@ As of 2025-05-17, 308 of the 379 products tracked by endoflife.date have automat
| Veeam Backup & Replication | [`/veeam-backup-and-replication`](https://endoflife.date/veeam-backup-and-replication) | ✔️ | veeam | | Veeam Backup & Replication | [`/veeam-backup-and-replication`](https://endoflife.date/veeam-backup-and-replication) | ✔️ | veeam |
| Veeam Backup for Microsoft 365 | [`/veeam-backup-for-microsoft-365`](https://endoflife.date/veeam-backup-for-microsoft-365) | ✔️ | veeam | | Veeam Backup for Microsoft 365 | [`/veeam-backup-for-microsoft-365`](https://endoflife.date/veeam-backup-for-microsoft-365) | ✔️ | veeam |
| Veeam ONE | [`/veeam-one`](https://endoflife.date/veeam-one) | ✔️ | veeam | | Veeam ONE | [`/veeam-one`](https://endoflife.date/veeam-one) | ✔️ | veeam |
| VirtualBox | [`/virtualbox`](https://endoflife.date/virtualbox) | | | | VirtualBox | [`/virtualbox`](https://endoflife.date/virtualbox) | ✔️ | virtualbox |
| Apple visionOS | [`/visionos`](https://endoflife.date/visionos) | ✔️ | apple | | Apple visionOS | [`/visionos`](https://endoflife.date/visionos) | ✔️ | apple |
| Visual COBOL | [`/visual-cobol`](https://endoflife.date/visual-cobol) | ❌ | | | Visual COBOL | [`/visual-cobol`](https://endoflife.date/visual-cobol) | ❌ | |
| Microsoft Visual Studio | [`/visual-studio`](https://endoflife.date/visual-studio) | ✔️ | custom | | Microsoft Visual Studio | [`/visual-studio`](https://endoflife.date/visual-studio) | ✔️ | visual-studio |
| Vitess | [`/vitess`](https://endoflife.date/vitess) | ✔️ | git | | Vitess | [`/vitess`](https://endoflife.date/vitess) | ✔️ | git |
| VMware Cloud Foundation | [`/vmware-cloud-foundation`](https://endoflife.date/vmware-cloud-foundation) | ❌ | | | VMware Cloud Foundation | [`/vmware-cloud-foundation`](https://endoflife.date/vmware-cloud-foundation) | ❌ | |
| VMware ESXi | [`/esxi`](https://endoflife.date/esxi) | ❌ | | | VMware ESXi | [`/esxi`](https://endoflife.date/esxi) | ❌ | |
@@ -389,19 +393,19 @@ As of 2025-05-17, 308 of the 379 products tracked by endoflife.date have automat
| Apple watchOS | [`/watchos`](https://endoflife.date/watchos) | ✔️ | apple | | Apple watchOS | [`/watchos`](https://endoflife.date/watchos) | ✔️ | apple |
| Weakforced | [`/weakforced`](https://endoflife.date/weakforced) | ✔️ | git | | Weakforced | [`/weakforced`](https://endoflife.date/weakforced) | ✔️ | git |
| WeeChat | [`/weechat`](https://endoflife.date/weechat) | ✔️ | git | | WeeChat | [`/weechat`](https://endoflife.date/weechat) | ✔️ | git |
| Microsoft Windows | [`/windows`](https://endoflife.date/windows) | ❌ | |
| Microsoft Windows Embedded | [`/windows-embedded`](https://endoflife.date/windows-embedded) | ❌ | | | Microsoft Windows Embedded | [`/windows-embedded`](https://endoflife.date/windows-embedded) | ❌ | |
| Microsoft Nano Server | [`/windows-nano-server`](https://endoflife.date/windows-nano-server) | ❌ | | | Microsoft Nano Server | [`/windows-nano-server`](https://endoflife.date/windows-nano-server) | ❌ | |
| Microsoft Windows Server Core | [`/windows-server-core`](https://endoflife.date/windows-server-core) | ❌ | |
| Microsoft Windows Server | [`/windows-server`](https://endoflife.date/windows-server) | ❌ | | | Microsoft Windows Server | [`/windows-server`](https://endoflife.date/windows-server) | ❌ | |
| Microsoft Windows | [`/windows`](https://endoflife.date/windows) | ❌ | | | Microsoft Windows Server Core | [`/windows-server-core`](https://endoflife.date/windows-server-core) | ❌ | |
| Wireshark | [`/wireshark`](https://endoflife.date/wireshark) | ✔️ | git | | Wireshark | [`/wireshark`](https://endoflife.date/wireshark) | ✔️ | git |
| WordPress | [`/wordpress`](https://endoflife.date/wordpress) | ✔️ | git | | WordPress | [`/wordpress`](https://endoflife.date/wordpress) | ✔️ | git |
| XCP-ng | [`/xcp-ng`](https://endoflife.date/xcp-ng) | ✔️ | git, release_table | | XCP-ng | [`/xcp-ng`](https://endoflife.date/xcp-ng) | ✔️ | git, release_table |
| Yarn | [`/yarn`](https://endoflife.date/yarn) | ✔️ | npm | | Yarn | [`/yarn`](https://endoflife.date/yarn) | ✔️ | npm |
| Yocto Project | [`/yocto`](https://endoflife.date/yocto) | ✔️ | git | | Yocto Project | [`/yocto`](https://endoflife.date/yocto) | ✔️ | git |
| Zabbix | [`/zabbix`](https://endoflife.date/zabbix) | ✔️ | git, release_table | | Zabbix | [`/zabbix`](https://endoflife.date/zabbix) | ✔️ | git |
| Zentyal | [`/zentyal`](https://endoflife.date/zentyal) | ✔️ | release_table | | Zentyal | [`/zentyal`](https://endoflife.date/zentyal) | ✔️ | release_table |
| Zerto | [`/zerto`](https://endoflife.date/zerto) | | | | Zerto | [`/zerto`](https://endoflife.date/zerto) | ✔️ | release_table |
| Apache ZooKeeper | [`/zookeeper`](https://endoflife.date/zookeeper) | ✔️ | maven | | Apache ZooKeeper | [`/zookeeper`](https://endoflife.date/zookeeper) | ✔️ | maven |
This table has been generated by [report.py](/report.py). This table has been generated by [report.py](/report.py).

View File

@@ -11,7 +11,9 @@ from ruamel.yaml import YAML
from ruamel.yaml.representer import RoundTripRepresenter from ruamel.yaml.representer import RoundTripRepresenter
from ruamel.yaml.resolver import Resolver from ruamel.yaml.resolver import Resolver
from src.common.endoflife import list_products
from src.common.gha import GitHubOutput from src.common.gha import GitHubOutput
from src.common.releasedata import DATA_DIR
""" """
Updates the `release`, `latest` and `latestReleaseDate` property in automatically updated pages Updates the `release`, `latest` and `latestReleaseDate` property in automatically updated pages
@@ -243,7 +245,6 @@ if __name__ == "__main__":
parser = argparse.ArgumentParser(description='Update product releases.') parser = argparse.ArgumentParser(description='Update product releases.')
parser.add_argument('product', nargs='?', help='restrict update to the given product') parser.add_argument('product', nargs='?', help='restrict update to the given product')
parser.add_argument('-p', '--product-dir', required=True, help='path to the product directory') parser.add_argument('-p', '--product-dir', required=True, help='path to the product directory')
parser.add_argument('-d', '--data-dir', required=True, help='path to the release data directory')
parser.add_argument('-v', '--verbose', action='store_true', help='enable verbose logging') parser.add_argument('-v', '--verbose', action='store_true', help='enable verbose logging')
args = parser.parse_args() args = parser.parse_args()
@@ -257,10 +258,11 @@ if __name__ == "__main__":
RoundTripRepresenter.ignore_aliases = lambda x, y: True # NOQA: ARG005 RoundTripRepresenter.ignore_aliases = lambda x, y: True # NOQA: ARG005
products_dir = Path(args.product_dir) products_dir = Path(args.product_dir)
product_names = [args.product] if args.product else [p.stem for p in products_dir.glob("*.md")] data_dir = Path(__file__).resolve().parent / DATA_DIR
products = list_products(products_dir, args.product)
github_output = GitHubOutput("warning") github_output = GitHubOutput("warning")
with github_output: with github_output:
for product_name in sorted(product_names): for product in products:
logging.debug(f"Processing {product_name}") logging.debug(f"Processing {product.name}")
update_product(product_name, products_dir, Path(args.data_dir), github_output) update_product(product.name, products_dir, data_dir, github_output)

View File

@@ -1,20 +1,28 @@
import argparse
import time import time
from pathlib import Path
from src.common import endoflife from src.common import endoflife
products = endoflife.list_products() if __name__ == "__main__":
count_auto = len([product for product in products if product.auto_configs()]) parser = argparse.ArgumentParser(description='Create report on product automation.')
parser.add_argument('-p', '--product-dir', required=True, help='path to the product directory')
args = parser.parse_args()
print(f"As of {time.strftime('%Y-%m-%d')}, {count_auto} of the {len(products)} products" products_dir = Path(args.product_dir)
f" tracked by endoflife.date have automatically tracked releases:") products = endoflife.list_products(products_dir)
print() count_auto = len([product for product in products if product.auto_configs()])
print('| Product | Permalink | Auto | Method(s) |')
print('|---------|-----------|------|-----------|') print(f"As of {time.strftime('%Y-%m-%d')}, {count_auto} of the {len(products)} products"
for product in products: f" tracked by endoflife.date have automatically tracked releases:")
title = product.get_title() print()
permalink = product.get_permalink() print('| Product | Permalink | Auto | Method(s) |')
auto = '✔️' if product.has_auto_configs() else '' print('|---------|-----------|------|-----------|')
methods = ', '.join(sorted({config.method for config in product.auto_configs()})) for product in products:
print(f"| {title} | [`{permalink}`](https://endoflife.date{permalink}) | {auto} | {methods} |") title = product.get_title()
print() permalink = product.get_permalink()
print('This table has been generated by [report.py](/report.py).') auto = '✔️' if product.has_auto_configs() else ''
methods = ', '.join(sorted({config.method for config in product.auto_configs()}))
print(f"| {title} | [`{permalink}`](https://endoflife.date{permalink}) | {auto} | {methods} |")
print()
print('This table has been generated by [report.py](/report.py).')

View File

@@ -1,11 +1,11 @@
import logging import logging
from common import dates, endoflife, http, releasedata from common import dates, http, releasedata
"""Fetches EKS versions from AWS docs. """Fetches EKS versions from AWS docs.
Now that AWS no longer publishes docs on GitHub, we use the Web Archive to get the older versions.""" Now that AWS no longer publishes docs on GitHub, we use the Web Archive to get the older versions."""
for config in endoflife.list_configs_from_argv(): for config in releasedata.list_configs_from_argv():
with releasedata.ProductData(config.product) as product_data: with releasedata.ProductData(config.product) as product_data:
html = http.fetch_html(config.url) html = http.fetch_html(config.url)

View File

@@ -1,10 +1,10 @@
import logging import logging
from common import dates, endoflife, http, releasedata from common import dates, http, releasedata
"""Fetches Amazon Neptune versions from its RSS feed on docs.aws.amazon.com.""" """Fetches Amazon Neptune versions from its RSS feed on docs.aws.amazon.com."""
for config in endoflife.list_configs_from_argv(): for config in releasedata.list_configs_from_argv():
with releasedata.ProductData(config.product) as product_data: with releasedata.ProductData(config.product) as product_data:
rss = http.fetch_xml(config.url) rss = http.fetch_xml(config.url)

View File

@@ -1,10 +1,10 @@
from common import dates, endoflife, releasedata from common import dates, releasedata
from common.git import Git from common.git import Git
"""Fetches Apache HTTP Server versions and release date from its git repository """Fetches Apache HTTP Server versions and release date from its git repository
by looking at the STATUS file of each <major>.<minor>.x branch.""" by looking at the STATUS file of each <major>.<minor>.x branch."""
for config in endoflife.list_configs_from_argv(): for config in releasedata.list_configs_from_argv():
with releasedata.ProductData(config.product) as product_data: with releasedata.ProductData(config.product) as product_data:
git = Git(config.url) git = Git(config.url)
git.setup() git.setup()

View File

@@ -1,8 +1,8 @@
import logging import logging
from common import dates, endoflife, http, releasedata from common import dates, http, releasedata
for config in endoflife.list_configs_from_argv(): for config in releasedata.list_configs_from_argv():
with releasedata.ProductData(config.product) as product_data: with releasedata.ProductData(config.product) as product_data:
html = http.fetch_html(config.url) html = http.fetch_html(config.url)

View File

@@ -2,7 +2,7 @@ import logging
import re import re
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from common import dates, endoflife, http, releasedata from common import dates, http, releasedata
"""Fetches and parses version and release date information from Apple's support website.""" """Fetches and parses version and release date information from Apple's support website."""
@@ -22,7 +22,7 @@ URLS = [
DATE_PATTERN = re.compile(r"\b\d+\s[A-Za-z]+\s\d+\b") DATE_PATTERN = re.compile(r"\b\d+\s[A-Za-z]+\s\d+\b")
for config in endoflife.list_configs_from_argv(): for config in releasedata.list_configs_from_argv():
with releasedata.ProductData(config.product) as product_data: with releasedata.ProductData(config.product) as product_data:
# URLs are cached to avoid rate limiting by support.apple.com. # URLs are cached to avoid rate limiting by support.apple.com.
soups = [BeautifulSoup(response.text, features="html5lib") for response in http.fetch_urls(URLS)] soups = [BeautifulSoup(response.text, features="html5lib") for response in http.fetch_urls(URLS)]

View File

@@ -1,10 +1,10 @@
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from common import dates, endoflife, http, releasedata from common import dates, http, releasedata
"""Fetches Artifactory versions from https://jfrog.com, using requests_html because JavaScript is """Fetches Artifactory versions from https://jfrog.com, using requests_html because JavaScript is
needed to render the page.""" needed to render the page."""
for config in endoflife.list_configs_from_argv(): for config in releasedata.list_configs_from_argv():
with releasedata.ProductData(config.product) as product_data: with releasedata.ProductData(config.product) as product_data:
content = http.fetch_javascript_url(config.url, wait_until = 'networkidle') content = http.fetch_javascript_url(config.url, wait_until = 'networkidle')
soup = BeautifulSoup(content, 'html.parser') soup = BeautifulSoup(content, 'html.parser')

View File

@@ -1,7 +1,7 @@
import logging import logging
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from common import dates, endoflife, http, releasedata from common import dates, http, releasedata
"""Fetches EOL dates from Atlassian EOL page. """Fetches EOL dates from Atlassian EOL page.
@@ -9,7 +9,7 @@ This script takes a selector argument which is the product title identifier on t
`AtlassianSupportEndofLifePolicy-JiraSoftware`. `AtlassianSupportEndofLifePolicy-JiraSoftware`.
""" """
for config in endoflife.list_configs_from_argv(): for config in releasedata.list_configs_from_argv():
with releasedata.ProductData(config.product) as product_data: with releasedata.ProductData(config.product) as product_data:
content = http.fetch_javascript_url(config.url) content = http.fetch_javascript_url(config.url)
soup = BeautifulSoup(content, features="html5lib") soup = BeautifulSoup(content, features="html5lib")

View File

@@ -1,5 +1,5 @@
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from common import dates, endoflife, http, releasedata from common import dates, http, releasedata
"""Fetches versions from Atlassian download-archives pages. """Fetches versions from Atlassian download-archives pages.
@@ -7,7 +7,7 @@ This script takes a single argument which is the url of the product's download-a
`https://www.atlassian.com/software/confluence/download-archives`. `https://www.atlassian.com/software/confluence/download-archives`.
""" """
for config in endoflife.list_configs_from_argv(): for config in releasedata.list_configs_from_argv():
with releasedata.ProductData(config.product) as product_data: with releasedata.ProductData(config.product) as product_data:
content = http.fetch_javascript_url(config.url, wait_until='networkidle') content = http.fetch_javascript_url(config.url, wait_until='networkidle')
soup = BeautifulSoup(content, 'html5lib') soup = BeautifulSoup(content, 'html5lib')

View File

@@ -1,10 +1,10 @@
import logging import logging
from common import dates, endoflife, http, releasedata from common import dates, http, releasedata
"""Fetches AWS lambda runtimes with their support / EOL dates from https://docs.aws.amazon.com.""" """Fetches AWS lambda runtimes with their support / EOL dates from https://docs.aws.amazon.com."""
for config in endoflife.list_configs_from_argv(): for config in releasedata.list_configs_from_argv():
with releasedata.ProductData(config.product) as product_data: with releasedata.ProductData(config.product) as product_data:
html = http.fetch_html(config.url) html = http.fetch_html(config.url)

View File

@@ -1,9 +1,9 @@
from common import dates, endoflife, http, releasedata from common import dates, http, releasedata
"""Fetches versions from repositories managed with cgit, such as the Linux kernel repository. """Fetches versions from repositories managed with cgit, such as the Linux kernel repository.
Ideally we would want to use the git repository directly, but cgit-managed repositories don't support partial clone.""" Ideally we would want to use the git repository directly, but cgit-managed repositories don't support partial clone."""
for config in endoflife.list_configs_from_argv(): for config in releasedata.list_configs_from_argv():
with releasedata.ProductData(config.product) as product_data: with releasedata.ProductData(config.product) as product_data:
html = http.fetch_html(config.url + '/refs/tags') html = http.fetch_html(config.url + '/refs/tags')

View File

@@ -1,4 +1,4 @@
from common import dates, endoflife, http, releasedata from common import dates, http, releasedata
from common.git import Git from common.git import Git
"""Fetch released versions from docs.chef.io and retrieve their date from GitHub. """Fetch released versions from docs.chef.io and retrieve their date from GitHub.
@@ -7,7 +7,7 @@ docs.chef.io needs to be scraped because not all tagged versions are actually re
More context on https://github.com/endoflife-date/endoflife.date/pull/4425#discussion_r1447932411. More context on https://github.com/endoflife-date/endoflife.date/pull/4425#discussion_r1447932411.
""" """
for config in endoflife.list_configs_from_argv(): for config in releasedata.list_configs_from_argv():
with releasedata.ProductData(config.product) as product_data: with releasedata.ProductData(config.product) as product_data:
html = http.fetch_html(config.url) html = http.fetch_html(config.url)
released_versions = [h2.get('id') for h2 in html.find_all('h2', id=True) if h2.get('id')] released_versions = [h2.get('id') for h2 in html.find_all('h2', id=True) if h2.get('id')]

View File

@@ -1,4 +1,4 @@
from common import dates, endoflife, github, http, releasedata from common import dates, github, http, releasedata
"""Fetch released versions from docs.chef.io and retrieve their date from GitHub. """Fetch released versions from docs.chef.io and retrieve their date from GitHub.
docs.chef.io needs to be scraped because not all tagged versions are actually released. docs.chef.io needs to be scraped because not all tagged versions are actually released.
@@ -6,7 +6,7 @@ docs.chef.io needs to be scraped because not all tagged versions are actually re
More context on https://github.com/endoflife-date/endoflife.date/pull/4425#discussion_r1447932411. More context on https://github.com/endoflife-date/endoflife.date/pull/4425#discussion_r1447932411.
""" """
for config in endoflife.list_configs_from_argv(): for config in releasedata.list_configs_from_argv():
with releasedata.ProductData(config.product) as product_data: with releasedata.ProductData(config.product) as product_data:
html = http.fetch_html(config.url) html = http.fetch_html(config.url)
released_versions = [h2.get('id') for h2 in html.find_all('h2', id=True) if h2.get('id')] released_versions = [h2.get('id') for h2 in html.find_all('h2', id=True) if h2.get('id')]

View File

@@ -1,6 +1,6 @@
import re import re
from common import dates, endoflife, http, releasedata from common import dates, http, releasedata
"""Fetches versions from Adobe ColdFusion release notes on helpx.adobe.com. """Fetches versions from Adobe ColdFusion release notes on helpx.adobe.com.
@@ -21,7 +21,7 @@ FIXED_VERSIONS = {
"2023.0.0": dates.date(2022, 5, 16), # https://coldfusion.adobe.com/2023/05/coldfusion2023-release/ "2023.0.0": dates.date(2022, 5, 16), # https://coldfusion.adobe.com/2023/05/coldfusion2023-release/
} }
for config in endoflife.list_configs_from_argv(): for config in releasedata.list_configs_from_argv():
with releasedata.ProductData(config.product) as product_data: with releasedata.ProductData(config.product) as product_data:
html = http.fetch_html(config.url) html = http.fetch_html(config.url)

View File

@@ -1,8 +1,5 @@
import itertools
import logging import logging
import os
import re import re
import sys
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
@@ -15,8 +12,6 @@ DEFAULT_VERSION_REGEX = r"^v?(?P<major>[1-9]\d*)\.(?P<minor>\d+)(\.(?P<patch>\d+
DEFAULT_VERSION_PATTERN = re.compile(DEFAULT_VERSION_REGEX) DEFAULT_VERSION_PATTERN = re.compile(DEFAULT_VERSION_REGEX)
DEFAULT_VERSION_TEMPLATE = "{{major}}{% if minor %}.{{minor}}{% if patch %}.{{patch}}{% if tiny %}.{{tiny}}{% endif %}{% endif %}{% endif %}" DEFAULT_VERSION_TEMPLATE = "{{major}}{% if minor %}.{{minor}}{% if patch %}.{{patch}}{% if tiny %}.{{tiny}}{% endif %}{% endif %}{% endif %}"
PRODUCTS_PATH = Path(os.environ.get("PRODUCTS_PATH", "website/products"))
class AutoConfig: class AutoConfig:
def __init__(self, product: str, data: dict) -> None: def __init__(self, product: str, data: dict) -> None:
@@ -58,9 +53,9 @@ class AutoConfig:
class ProductFrontmatter: class ProductFrontmatter:
def __init__(self, name: str) -> None: def __init__(self, path: Path) -> None:
self.name: str = name self.path: Path = path
self.path: Path = PRODUCTS_PATH / f"{name}.md" self.name: str = path.stem
self.data = None self.data = None
if self.path.is_file(): if self.path.is_file():
@@ -109,37 +104,19 @@ class ProductFrontmatter:
return None return None
def list_products(products_filter: str = None) -> list[ProductFrontmatter]: def list_products(products_dir: Path, product_name: str = None) -> list[ProductFrontmatter]:
"""Return a list of products that are using the same given update method.""" product_names = [product_name] if product_name else sorted([p.stem for p in products_dir.glob("*.md")])
products = [] products = []
for product_name in product_names:
for product_file in sorted(PRODUCTS_PATH.glob("*.md")):
product_name = product_file.stem
if products_filter and product_name != products_filter:
continue
try: try:
products.append(ProductFrontmatter(product_name)) products.append(ProductFrontmatter(products_dir / f"{product_name}.md"))
except Exception as e: except Exception as e:
logging.exception(f"failed to load product data for {product_name}: {e}") logging.exception(f"failed to load product data for {product_name}: {e}")
return products return products
def list_configs(products_filter: str = None, methods_filter: str = None, urls_filter: str = None) -> list[AutoConfig]:
"""Return a list of auto configs, filtering by product name, method, and URL."""
products = list_products(products_filter)
configs_by_product = [p.auto_configs(methods_filter, urls_filter) for p in products]
return list(itertools.chain.from_iterable(configs_by_product)) # flatten the list of lists
def list_configs_from_argv() -> list[AutoConfig]:
products_filter = sys.argv[1] if len(sys.argv) > 1 else None
methods_filter = sys.argv[2] if len(sys.argv) > 1 else None
urls_filter = sys.argv[3] if len(sys.argv) > 2 else None
return list_configs(products_filter, methods_filter, urls_filter)
def to_identifier(s: str) -> str: def to_identifier(s: str) -> str:
"""Convert a string to a valid endoflife.date identifier.""" """Convert a string to a valid endoflife.date identifier."""
identifier = s.strip().lower() identifier = s.strip().lower()

View File

@@ -1,15 +1,16 @@
import argparse
import json import json
import logging import logging
import os import sys
from datetime import datetime, timezone from datetime import datetime, timezone
from pathlib import Path from pathlib import Path
from types import TracebackType from types import TracebackType
from typing import Optional, Type from typing import Optional, Type
# Do not update the format: it's also used to declare groups in the GitHub Actions logs. from . import endoflife
logging.basicConfig(format="%(message)s", level=logging.INFO)
VERSIONS_PATH = Path(os.environ.get("VERSIONS_PATH", "releases")) SRC_DIR = Path('src')
DATA_DIR = Path('releases')
class ProductUpdateError(Exception): class ProductUpdateError(Exception):
@@ -108,7 +109,7 @@ class ProductVersion:
class ProductData: class ProductData:
def __init__(self, name: str) -> None: def __init__(self, name: str) -> None:
self.name: str = name self.name: str = name
self.versions_path: Path = VERSIONS_PATH / f"{name}.json" self.versions_path: Path = DATA_DIR / f"{name}.json"
self.releases = {} self.releases = {}
self.versions: dict[str, ProductVersion] = {} self.versions: dict[str, ProductVersion] = {}
self.updated = False self.updated = False
@@ -190,3 +191,21 @@ class ProductData:
def __repr__(self) -> str: def __repr__(self) -> str:
return self.name return self.name
def list_configs_from_argv() -> list[endoflife.AutoConfig]:
return parse_argv()[1]
def parse_argv() -> tuple[endoflife.ProductFrontmatter, list[endoflife.AutoConfig]]:
parser = argparse.ArgumentParser(description=sys.argv[0])
parser.add_argument('-p', '--product', required=True, help='path to the product')
parser.add_argument('-m', '--method', required=True, help='method to filter by')
parser.add_argument('-u', '--url', required=True, help='url to filter by')
parser.add_argument('-v', '--verbose', action='store_true', help='enable verbose logging')
args = parser.parse_args()
# Do not update the format: it's also used to declare groups in the GitHub Actions logs.
logging.basicConfig(format="%(message)s", level=(logging.DEBUG if args.verbose else logging.INFO))
product = endoflife.ProductFrontmatter(Path(args.product))
return product, product.auto_configs(args.method, args.url)

View File

@@ -2,7 +2,7 @@ import datetime
import re import re
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from common import dates, endoflife, http, releasedata from common import dates, http, releasedata
MILESTONE_PATTERN = re.compile(r'COS \d+ LTS') MILESTONE_PATTERN = re.compile(r'COS \d+ LTS')
VERSION_PATTERN = re.compile(r"^(cos-\d+-\d+-\d+-\d+)") VERSION_PATTERN = re.compile(r"^(cos-\d+-\d+-\d+-\d+)")
@@ -14,7 +14,7 @@ def parse_date(date_text: str) -> datetime:
return dates.parse_date(date_text) return dates.parse_date(date_text)
for config in endoflife.list_configs_from_argv(): for config in releasedata.list_configs_from_argv():
with releasedata.ProductData(config.product) as product_data: with releasedata.ProductData(config.product) as product_data:
main = http.fetch_url(config.url) main = http.fetch_url(config.url)
main_soup = BeautifulSoup(main.text, features="html5lib") main_soup = BeautifulSoup(main.text, features="html5lib")

View File

@@ -1,7 +1,7 @@
import logging import logging
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from common import dates, endoflife, http, releasedata from common import dates, http, releasedata
"""Fetches versions from release notes of each minor version on docs.couchbase.com. """Fetches versions from release notes of each minor version on docs.couchbase.com.
@@ -16,7 +16,7 @@ MANUAL_VERSIONS = {
"7.2.0": dates.date(2023, 6, 1), # https://www.couchbase.com/blog/couchbase-capella-spring-release-72/ "7.2.0": dates.date(2023, 6, 1), # https://www.couchbase.com/blog/couchbase-capella-spring-release-72/
} }
for config in endoflife.list_configs_from_argv(): for config in releasedata.list_configs_from_argv():
with releasedata.ProductData(config.product) as product_data: with releasedata.ProductData(config.product) as product_data:
html = http.fetch_html(f"{config.url}/current/install/install-intro.html") html = http.fetch_html(f"{config.url}/current/install/install-intro.html")

View File

@@ -1,7 +1,7 @@
from pathlib import Path from pathlib import Path
from subprocess import run from subprocess import run
from common import dates, endoflife, releasedata from common import dates, releasedata
from common.git import Git from common.git import Git
"""Fetch Debian versions by parsing news in www.debian.org source repository.""" """Fetch Debian versions by parsing news in www.debian.org source repository."""
@@ -40,7 +40,7 @@ def extract_point_versions(p: releasedata.ProductData, repo_dir: Path) -> None:
(date, version) = line.split(' ') (date, version) = line.split(' ')
p.declare_version(version, dates.parse_date(date)) p.declare_version(version, dates.parse_date(date))
for config in endoflife.list_configs_from_argv(): for config in releasedata.list_configs_from_argv():
with releasedata.ProductData(config.product) as product_data: with releasedata.ProductData(config.product) as product_data:
git = Git(config.url) git = Git(config.url)
git.setup() git.setup()

View File

@@ -1,6 +1,6 @@
from common import dates, endoflife, http, releasedata from common import dates, http, releasedata
for config in endoflife.list_configs_from_argv(): for config in releasedata.list_configs_from_argv():
with releasedata.ProductData(config.product) as product_data: with releasedata.ProductData(config.product) as product_data:
html = http.fetch_html(f"https://distrowatch.com/index.php?distribution={config.url}") html = http.fetch_html(f"https://distrowatch.com/index.php?distribution={config.url}")

View File

@@ -17,6 +17,6 @@ def fetch_releases(p: releasedata.ProductData, c: endoflife.AutoConfig, url: str
fetch_releases(p, c, data["next"]) fetch_releases(p, c, data["next"])
for config in endoflife.list_configs_from_argv(): for config in releasedata.list_configs_from_argv():
with releasedata.ProductData(config.product) as product_data: with releasedata.ProductData(config.product) as product_data:
fetch_releases(product_data, config, f"https://hub.docker.com/v2/repositories/{config.url}/tags?page_size=100&page=1") fetch_releases(product_data, config, f"https://hub.docker.com/v2/repositories/{config.url}/tags?page_size=100&page=1")

View File

@@ -1,7 +1,7 @@
import urllib.parse import urllib.parse
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from common import dates, endoflife, http, releasedata from common import dates, http, releasedata
"""Fetch Firefox versions with their dates from https://www.mozilla.org/. """Fetch Firefox versions with their dates from https://www.mozilla.org/.
@@ -20,7 +20,7 @@ The script will need to be updated if someday those conditions are not met."""
MAX_VERSIONS_LIMIT = 100 MAX_VERSIONS_LIMIT = 100
for config in endoflife.list_configs_from_argv(): for config in releasedata.list_configs_from_argv():
with releasedata.ProductData(config.product) as product_data: with releasedata.ProductData(config.product) as product_data:
releases_page = http.fetch_url(config.url) releases_page = http.fetch_url(config.url)
releases_soup = BeautifulSoup(releases_page.text, features="html5lib") releases_soup = BeautifulSoup(releases_page.text, features="html5lib")

View File

@@ -14,7 +14,7 @@ References:
import re import re
from typing import Any, Generator, Iterator from typing import Any, Generator, Iterator
from common import dates, endoflife, http, releasedata from common import dates, http, releasedata
def parse_markdown_tables(lineiter: Iterator[str]) -> Generator[list[list[Any]], Any, None]: def parse_markdown_tables(lineiter: Iterator[str]) -> Generator[list[list[Any]], Any, None]:
@@ -50,7 +50,7 @@ def maybe_markdown_table_row(line: str) -> list[str] | None:
return None return None
return [x.strip() for x in line.strip('|').split('|')] return [x.strip() for x in line.strip('|').split('|')]
for config in endoflife.list_configs_from_argv(): for config in releasedata.list_configs_from_argv():
with releasedata.ProductData(config.product) as product: with releasedata.ProductData(config.product) as product:
resp = http.fetch_url(config.url) resp = http.fetch_url(config.url)
resp.raise_for_status() resp.raise_for_status()

View File

@@ -1,9 +1,9 @@
from common import dates, endoflife, releasedata from common import dates, releasedata
from common.git import Git from common.git import Git
"""Fetches versions from tags in a git repository. This replace the old update.rb script.""" """Fetches versions from tags in a git repository. This replace the old update.rb script."""
for config in endoflife.list_configs_from_argv(): for config in releasedata.list_configs_from_argv():
with releasedata.ProductData(config.product) as product_data: with releasedata.ProductData(config.product) as product_data:
git = Git(config.url) git = Git(config.url)
git.setup(bare=True) git.setup(bare=True)

View File

@@ -1,11 +1,11 @@
from common import dates, endoflife, github, releasedata from common import dates, github, releasedata
"""Fetches versions from GitHub releases using the GraphQL API and the GitHub CLI. """Fetches versions from GitHub releases using the GraphQL API and the GitHub CLI.
Note: GraphQL API and GitHub CLI are used because it's simpler: no need to manage pagination and authentication. Note: GraphQL API and GitHub CLI are used because it's simpler: no need to manage pagination and authentication.
""" """
for config in endoflife.list_configs_from_argv(): for config in releasedata.list_configs_from_argv():
with releasedata.ProductData(config.product) as product_data: with releasedata.ProductData(config.product) as product_data:
for release in github.fetch_releases(config.url): for release in github.fetch_releases(config.url):
if release.is_prerelease: if release.is_prerelease:

View File

@@ -1,11 +1,11 @@
from common import dates, endoflife, github, releasedata from common import dates, github, releasedata
"""Fetches versions from GitHub tags using the GraphQL API and the GitHub CLI. """Fetches versions from GitHub tags using the GraphQL API and the GitHub CLI.
Note: GraphQL API and GitHub CLI are used because it's simpler: no need to manage pagination and authentication. Note: GraphQL API and GitHub CLI are used because it's simpler: no need to manage pagination and authentication.
""" """
for config in endoflife.list_configs_from_argv(): for config in releasedata.list_configs_from_argv():
with releasedata.ProductData(config.product) as product_data: with releasedata.ProductData(config.product) as product_data:
for tag in github.fetch_tags(config.url): for tag in github.fetch_tags(config.url):
version_str = tag.name version_str = tag.name

View File

@@ -1,6 +1,6 @@
import re import re
from common import dates, endoflife, http, releasedata from common import dates, http, releasedata
# https://regex101.com/r/zPxBqT/1 # https://regex101.com/r/zPxBqT/1
VERSION_PATTERN = re.compile(r"\d.\d+\.\d+-gke\.\d+") VERSION_PATTERN = re.compile(r"\d.\d+\.\d+-gke\.\d+")
@@ -11,7 +11,7 @@ URL_BY_PRODUCT = {
"google-kubernetes-engine-rapid": "https://cloud.google.com/kubernetes-engine/docs/release-notes-rapid", "google-kubernetes-engine-rapid": "https://cloud.google.com/kubernetes-engine/docs/release-notes-rapid",
} }
for config in endoflife.list_configs_from_argv(): # noqa: B007 multiple JSON produced for historical reasons for config in releasedata.list_configs_from_argv(): # noqa: B007 multiple JSON produced for historical reasons
for product_name, url in URL_BY_PRODUCT.items(): for product_name, url in URL_BY_PRODUCT.items():
with releasedata.ProductData(product_name) as product_data: with releasedata.ProductData(product_name) as product_data:
html = http.fetch_html(url) html = http.fetch_html(url)

View File

@@ -1,8 +1,8 @@
import logging import logging
from common import dates, endoflife, http, releasedata from common import dates, http, releasedata
for config in endoflife.list_configs_from_argv(): for config in releasedata.list_configs_from_argv():
with releasedata.ProductData(config.product) as product_data: with releasedata.ProductData(config.product) as product_data:
html = http.fetch_html(config.url) html = http.fetch_html(config.url)
table_selector = config.data.get("table_selector", "#previous-releases + table").strip() table_selector = config.data.get("table_selector", "#previous-releases + table").strip()

View File

@@ -1,11 +1,11 @@
import re import re
from common import dates, endoflife, http, releasedata from common import dates, http, releasedata
CYCLE_PATTERN = re.compile(r"^(\d+\.\d+)/$") CYCLE_PATTERN = re.compile(r"^(\d+\.\d+)/$")
DATE_AND_VERSION_PATTERN = re.compile(r"^(\d{4})/(\d{2})/(\d{2})\s+:\s+(\d+\.\d+\.\d.?)$") # https://regex101.com/r/1JCnFC/1 DATE_AND_VERSION_PATTERN = re.compile(r"^(\d{4})/(\d{2})/(\d{2})\s+:\s+(\d+\.\d+\.\d.?)$") # https://regex101.com/r/1JCnFC/1
for config in endoflife.list_configs_from_argv(): for config in releasedata.list_configs_from_argv():
with releasedata.ProductData(config.product) as product_data: with releasedata.ProductData(config.product) as product_data:
# First, get all minor releases from the download page # First, get all minor releases from the download page
download_html = http.fetch_html(config.url) download_html = http.fetch_html(config.url)

View File

@@ -1,6 +1,6 @@
from common import dates, endoflife, http, releasedata from common import dates, http, releasedata
for config in endoflife.list_configs_from_argv(): for config in releasedata.list_configs_from_argv():
with releasedata.ProductData(config.product) as product_data: with releasedata.ProductData(config.product) as product_data:
html = http.fetch_html(config.url) html = http.fetch_html(config.url)

View File

@@ -1,6 +1,6 @@
import logging import logging
from common import dates, endoflife, http, releasedata from common import dates, http, releasedata
"""Fetch version data for Kuma from https://raw.githubusercontent.com/kumahq/kuma/master/versions.yml. """Fetch version data for Kuma from https://raw.githubusercontent.com/kumahq/kuma/master/versions.yml.
""" """
@@ -9,7 +9,7 @@ RELEASE_FIELD = 'release'
RELEASE_DATE_FIELD = 'releaseDate' RELEASE_DATE_FIELD = 'releaseDate'
EOL_FIELD = 'endOfLifeDate' EOL_FIELD = 'endOfLifeDate'
for config in endoflife.list_configs_from_argv(): for config in releasedata.list_configs_from_argv():
with releasedata.ProductData(config.product) as product_data: with releasedata.ProductData(config.product) as product_data:
versions_data = http.fetch_yaml(config.url) versions_data = http.fetch_yaml(config.url)

View File

@@ -1,10 +1,10 @@
import logging import logging
from common import dates, endoflife, http, releasedata from common import dates, http, releasedata
"""Fetches LibreOffice versions from https://downloadarchive.documentfoundation.org/libreoffice/old/""" """Fetches LibreOffice versions from https://downloadarchive.documentfoundation.org/libreoffice/old/"""
for config in endoflife.list_configs_from_argv(): for config in releasedata.list_configs_from_argv():
with releasedata.ProductData(config.product) as product_data: with releasedata.ProductData(config.product) as product_data:
html = http.fetch_html(config.url) html = http.fetch_html(config.url)

View File

@@ -1,14 +1,14 @@
import re import re
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from common import dates, endoflife, http, releasedata from common import dates, http, releasedata
"""Fetch Looker versions from the Google Cloud release notes RSS feed. """Fetch Looker versions from the Google Cloud release notes RSS feed.
""" """
ANNOUNCEMENT_PATTERN = re.compile(r"includes\s+the\s+following\s+changes", re.IGNORECASE) ANNOUNCEMENT_PATTERN = re.compile(r"includes\s+the\s+following\s+changes", re.IGNORECASE)
for config in endoflife.list_configs_from_argv(): for config in releasedata.list_configs_from_argv():
with releasedata.ProductData(config.product) as product_data: with releasedata.ProductData(config.product) as product_data:
rss = http.fetch_xml(config.url) rss = http.fetch_xml(config.url)

View File

@@ -1,13 +1,13 @@
import re import re
from common import dates, endoflife, http, releasedata from common import dates, http, releasedata
"""Fetches Lua releases from lua.org.""" """Fetches Lua releases from lua.org."""
RELEASED_AT_PATTERN = re.compile(r"Lua\s*(?P<release>\d+\.\d+)\s*was\s*released\s*on\s*(?P<release_date>\d+\s*\w+\s*\d{4})") RELEASED_AT_PATTERN = re.compile(r"Lua\s*(?P<release>\d+\.\d+)\s*was\s*released\s*on\s*(?P<release_date>\d+\s*\w+\s*\d{4})")
VERSION_PATTERN = re.compile(r"(?P<version>\d+\.\d+\.\d+),\s*released\s*on\s*(?P<version_date>\d+\s*\w+\s*\d{4})") VERSION_PATTERN = re.compile(r"(?P<version>\d+\.\d+\.\d+),\s*released\s*on\s*(?P<version_date>\d+\s*\w+\s*\d{4})")
for config in endoflife.list_configs_from_argv(): for config in releasedata.list_configs_from_argv():
with releasedata.ProductData(config.product) as product_data: with releasedata.ProductData(config.product) as product_data:
html = http.fetch_html(config.url, features = 'html.parser') html = http.fetch_html(config.url, features = 'html.parser')
page_text = html.text # HTML is broken, no way to parse it with beautifulsoup page_text = html.text # HTML is broken, no way to parse it with beautifulsoup

View File

@@ -1,8 +1,8 @@
from datetime import datetime, timezone from datetime import datetime, timezone
from common import endoflife, http, releasedata from common import http, releasedata
for config in endoflife.list_configs_from_argv(): for config in releasedata.list_configs_from_argv():
with releasedata.ProductData(config.product) as product_data: with releasedata.ProductData(config.product) as product_data:
start = 0 start = 0
group_id, artifact_id = config.url.split("/") group_id, artifact_id = config.url.split("/")

View File

@@ -1,10 +1,10 @@
import logging import logging
from common import dates, endoflife, http, releasedata from common import dates, http, releasedata
"""Fetches NetBSD versions and EOL information from https://www.netbsd.org/.""" """Fetches NetBSD versions and EOL information from https://www.netbsd.org/."""
for config in endoflife.list_configs_from_argv(): for config in releasedata.list_configs_from_argv():
with releasedata.ProductData(config.product) as product_data: with releasedata.ProductData(config.product) as product_data:
html = http.fetch_html(config.url) html = http.fetch_html(config.url)

View File

@@ -1,6 +1,6 @@
from common import dates, endoflife, http, releasedata from common import dates, http, releasedata
for config in endoflife.list_configs_from_argv(): for config in releasedata.list_configs_from_argv():
with releasedata.ProductData(config.product) as product_data: with releasedata.ProductData(config.product) as product_data:
data = http.fetch_json(f"https://registry.npmjs.org/{config.url}") data = http.fetch_json(f"https://registry.npmjs.org/{config.url}")
for version_str in data["versions"]: for version_str in data["versions"]:

View File

@@ -1,8 +1,8 @@
from common import dates, endoflife, http, releasedata from common import dates, http, releasedata
"""Fetch Nutanix products versions from https://portal.nutanix.com/api/v1.""" """Fetch Nutanix products versions from https://portal.nutanix.com/api/v1."""
for config in endoflife.list_configs_from_argv(): for config in releasedata.list_configs_from_argv():
with releasedata.ProductData(config.product) as product_data: with releasedata.ProductData(config.product) as product_data:
data = http.fetch_json(f"https://portal.nutanix.com/api/v1/eol/find?type={config.url}") data = http.fetch_json(f"https://portal.nutanix.com/api/v1/eol/find?type={config.url}")

View File

@@ -1,11 +1,11 @@
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from common import dates, endoflife, http, releasedata from common import dates, http, releasedata
"""Fetch Java versions from https://www.java.com/releases/. """Fetch Java versions from https://www.java.com/releases/.
This script is using requests-html because the page needs JavaScript to render correctly.""" This script is using requests-html because the page needs JavaScript to render correctly."""
for config in endoflife.list_configs_from_argv(): for config in releasedata.list_configs_from_argv():
with releasedata.ProductData(config.product) as product_data: with releasedata.ProductData(config.product) as product_data:
content = http.fetch_javascript_url(config.url, wait_until='networkidle') content = http.fetch_javascript_url(config.url, wait_until='networkidle')
soup = BeautifulSoup(content, 'html.parser') soup = BeautifulSoup(content, 'html.parser')

View File

@@ -1,8 +1,8 @@
from common import dates, endoflife, http, releasedata from common import dates, http, releasedata
"""Fetches pan-os versions from https://github.com/mrjcap/panos-versions/.""" """Fetches pan-os versions from https://github.com/mrjcap/panos-versions/."""
for config in endoflife.list_configs_from_argv(): for config in releasedata.list_configs_from_argv():
with releasedata.ProductData(config.product) as product_data: with releasedata.ProductData(config.product) as product_data:
versions = http.fetch_json(config.url) versions = http.fetch_json(config.url)

View File

@@ -1,6 +1,6 @@
from common import dates, endoflife, http, releasedata from common import dates, http, releasedata
for config in endoflife.list_configs_from_argv(): for config in releasedata.list_configs_from_argv():
with releasedata.ProductData(config.product) as product_data: with releasedata.ProductData(config.product) as product_data:
# Fetch major versions # Fetch major versions
latest_by_major = http.fetch_url(config.url).json() latest_by_major = http.fetch_url(config.url).json()

View File

@@ -1,11 +1,11 @@
from common import dates, endoflife, http, releasedata from common import dates, http, releasedata
"""Fetches versions from Plesk's change log. """Fetches versions from Plesk's change log.
Only 18.0.20.3 and later will be picked up, as the format of the change log for 18.0.20 and 18.0.19 are different and Only 18.0.20.3 and later will be picked up, as the format of the change log for 18.0.20 and 18.0.19 are different and
there is no entry for GA of version 18.0.18 and older.""" there is no entry for GA of version 18.0.18 and older."""
for config in endoflife.list_configs_from_argv(): for config in releasedata.list_configs_from_argv():
with releasedata.ProductData(config.product) as product_data: with releasedata.ProductData(config.product) as product_data:
html = http.fetch_html(config.url) html = http.fetch_html(config.url)

View File

@@ -1,6 +1,6 @@
from common import dates, endoflife, http, releasedata from common import dates, http, releasedata
for config in endoflife.list_configs_from_argv(): for config in releasedata.list_configs_from_argv():
with releasedata.ProductData(config.product) as product_data: with releasedata.ProductData(config.product) as product_data:
data = http.fetch_json(f"https://pypi.org/pypi/{config.url}/json") data = http.fetch_json(f"https://pypi.org/pypi/{config.url}/json")

View File

@@ -1,6 +1,6 @@
import logging import logging
from common import dates, endoflife, http, releasedata from common import dates, http, releasedata
"""Fetches Amazon RDS versions from the version management pages on AWS docs. """Fetches Amazon RDS versions from the version management pages on AWS docs.
@@ -8,7 +8,7 @@ Pages parsed by this script are expected to have version tables with a version i
in the third column (usually named 'RDS release date'). in the third column (usually named 'RDS release date').
""" """
for config in endoflife.list_configs_from_argv(): for config in releasedata.list_configs_from_argv():
with releasedata.ProductData(config.product) as product_data: with releasedata.ProductData(config.product) as product_data:
html = http.fetch_html(config.url) html = http.fetch_html(config.url)

View File

@@ -1,10 +1,10 @@
import logging import logging
from common import dates, endoflife, http, releasedata from common import dates, http, releasedata
"""Fetches RedHat JBoss EAP version data for JBoss 7""" """Fetches RedHat JBoss EAP version data for JBoss 7"""
for config in endoflife.list_configs_from_argv(): for config in releasedata.list_configs_from_argv():
with releasedata.ProductData(config.product) as product_data: with releasedata.ProductData(config.product) as product_data:
html = http.fetch_html(config.url) html = http.fetch_html(config.url)

View File

@@ -1,10 +1,10 @@
import re import re
from common import dates, endoflife, http, releasedata from common import dates, http, releasedata
"""Fetches the latest RedHat JBoss EAP version data for JBoss 8.0""" """Fetches the latest RedHat JBoss EAP version data for JBoss 8.0"""
for config in endoflife.list_configs_from_argv(): for config in releasedata.list_configs_from_argv():
with releasedata.ProductData(config.product) as product_data: with releasedata.ProductData(config.product) as product_data:
xml = http.fetch_xml(config.url) xml = http.fetch_xml(config.url)

View File

@@ -1,6 +1,6 @@
import re import re
from common import dates, endoflife, releasedata from common import dates, releasedata
from common.git import Git from common.git import Git
"""Fetches Red Hat OpenShift versions from the documentation's git repository""" """Fetches Red Hat OpenShift versions from the documentation's git repository"""
@@ -10,7 +10,7 @@ VERSION_AND_DATE_PATTERN = re.compile(
re.MULTILINE, re.MULTILINE,
) )
for config in endoflife.list_configs_from_argv(): for config in releasedata.list_configs_from_argv():
with releasedata.ProductData(config.product) as product_data: with releasedata.ProductData(config.product) as product_data:
git = Git(config.url) git = Git(config.url)
git.setup() git.setup()

View File

@@ -1,12 +1,12 @@
import logging import logging
from common import dates, endoflife, http, releasedata from common import dates, http, releasedata
"""Fetches Satellite versions from access.redhat.com. """Fetches Satellite versions from access.redhat.com.
A few of the older versions, such as 'Satellite 6.1 GA Release (Build 6.1.1)', were ignored because too hard to parse.""" A few of the older versions, such as 'Satellite 6.1 GA Release (Build 6.1.1)', were ignored because too hard to parse."""
for config in endoflife.list_configs_from_argv(): for config in releasedata.list_configs_from_argv():
with releasedata.ProductData(config.product) as product_data: with releasedata.ProductData(config.product) as product_data:
html = http.fetch_html(config.url) html = http.fetch_html(config.url)

View File

@@ -1,7 +1,7 @@
import logging import logging
import urllib.parse import urllib.parse
from common import dates, endoflife, http, releasedata from common import dates, http, releasedata
"""Fetches EOL dates from the Red Hat Product Life Cycle Data API. """Fetches EOL dates from the Red Hat Product Life Cycle Data API.
@@ -17,7 +17,7 @@ class Mapping:
def get_field_for(self, phase_name: str) -> str | None: def get_field_for(self, phase_name: str) -> str | None:
return self.fields_by_phase.get(phase_name.lower(), None) return self.fields_by_phase.get(phase_name.lower(), None)
for config in endoflife.list_configs_from_argv(): for config in releasedata.list_configs_from_argv():
with releasedata.ProductData(config.product) as product_data: with releasedata.ProductData(config.product) as product_data:
name = urllib.parse.quote(config.url) name = urllib.parse.quote(config.url)
mapping = Mapping(config.data["fields"]) mapping = Mapping(config.data["fields"])

View File

@@ -150,7 +150,7 @@ class Field:
return f"{self.name}({self.column})" return f"{self.name}({self.column})"
for config in endoflife.list_configs_from_argv(): for config in releasedata.list_configs_from_argv():
with releasedata.ProductData(config.product) as product_data: with releasedata.ProductData(config.product) as product_data:
render_javascript = config.data.get("render_javascript", False) render_javascript = config.data.get("render_javascript", False)
render_javascript_click_selector = config.data.get("render_javascript_click_selector", None) render_javascript_click_selector = config.data.get("render_javascript_click_selector", None)

View File

@@ -1,11 +1,11 @@
import re import re
from common import dates, endoflife, http, releasedata from common import dates, http, releasedata
# https://regex101.com/r/877ibq/1 # https://regex101.com/r/877ibq/1
VERSION_PATTERN = re.compile(r"RHEL (?P<major>\d)(\. ?(?P<minor>\d+))?(( Update (?P<minor2>\d))| GA)?") VERSION_PATTERN = re.compile(r"RHEL (?P<major>\d)(\. ?(?P<minor>\d+))?(( Update (?P<minor2>\d))| GA)?")
for config in endoflife.list_configs_from_argv(): for config in releasedata.list_configs_from_argv():
with releasedata.ProductData(config.product) as product_data: with releasedata.ProductData(config.product) as product_data:
html = http.fetch_html(config.url) html = http.fetch_html(config.url)

View File

@@ -1,6 +1,6 @@
from common import dates, endoflife, http, releasedata from common import dates, http, releasedata
for config in endoflife.list_configs_from_argv(): for config in releasedata.list_configs_from_argv():
with releasedata.ProductData(config.product) as product_data: with releasedata.ProductData(config.product) as product_data:
response = http.fetch_url(config.url) response = http.fetch_url(config.url)
for line in response.text.strip().split('\n'): for line in response.text.strip().split('\n'):

View File

@@ -1,8 +1,8 @@
import logging import logging
from common import dates, endoflife, http, releasedata from common import dates, http, releasedata
for config in endoflife.list_configs_from_argv(): for config in releasedata.list_configs_from_argv():
with releasedata.ProductData(config.product) as product_data: with releasedata.ProductData(config.product) as product_data:
html = http.fetch_html(config.url) html = http.fetch_html(config.url)

View File

@@ -12,9 +12,9 @@ it retains the date and use it as the model's EOL date.
TODAY = dates.today() TODAY = dates.today()
for config in endoflife.list_configs_from_argv(): frontmatter, configs = releasedata.parse_argv()
for config in configs:
with releasedata.ProductData(config.product) as product_data: with releasedata.ProductData(config.product) as product_data:
frontmatter = endoflife.ProductFrontmatter(product_data.name)
frontmatter_release_names = frontmatter.get_release_names() frontmatter_release_names = frontmatter.get_release_names()
# Copy EOL dates from frontmatter to product data # Copy EOL dates from frontmatter to product data

View File

@@ -1,8 +1,8 @@
import logging import logging
from common import dates, endoflife, http, releasedata from common import dates, http, releasedata
for config in endoflife.list_configs_from_argv(): for config in releasedata.list_configs_from_argv():
with releasedata.ProductData(config.product) as product_data: with releasedata.ProductData(config.product) as product_data:
html = http.fetch_html(config.url) html = http.fetch_html(config.url)

View File

@@ -1,6 +1,6 @@
import re import re
from common import dates, endoflife, http, releasedata from common import dates, http, releasedata
VERSION_DATE_PATTERN = re.compile(r"Splunk Enterprise (?P<version>\d+\.\d+(?:\.\d+)*) was (?:first )?released on (?P<date>\w+\s\d\d?,\s\d{4})\.", re.MULTILINE) VERSION_DATE_PATTERN = re.compile(r"Splunk Enterprise (?P<version>\d+\.\d+(?:\.\d+)*) was (?:first )?released on (?P<date>\w+\s\d\d?,\s\d{4})\.", re.MULTILINE)
@@ -29,7 +29,7 @@ def get_latest_minor_versions(versions: list[str]) -> list[str]:
return latest_versions return latest_versions
for config in endoflife.list_configs_from_argv(): for config in releasedata.list_configs_from_argv():
with releasedata.ProductData(config.product) as product_data: with releasedata.ProductData(config.product) as product_data:
html = http.fetch_html(config.url) html = http.fetch_html(config.url)

View File

@@ -1,6 +1,6 @@
from common import dates, endoflife, http, releasedata from common import dates, http, releasedata
for config in endoflife.list_configs_from_argv(): for config in releasedata.list_configs_from_argv():
with releasedata.ProductData(config.product) as product_data: with releasedata.ProductData(config.product) as product_data:
data = http.fetch_json(config.url) data = http.fetch_json(config.url)
for v in data: for v in data:

View File

@@ -1,4 +1,4 @@
from common import dates, endoflife, http, releasedata from common import dates, http, releasedata
"""Fetches the Unity LTS releases from the Unity website. Non-LTS releases are not listed there, so this automation """Fetches the Unity LTS releases from the Unity website. Non-LTS releases are not listed there, so this automation
is only partial. is only partial.
@@ -16,7 +16,7 @@ Note that it was assumed that:
The script will need to be updated if someday those conditions are not met.""" The script will need to be updated if someday those conditions are not met."""
for config in endoflife.list_configs_from_argv(): for config in releasedata.list_configs_from_argv():
with releasedata.ProductData(config.product) as product_data: with releasedata.ProductData(config.product) as product_data:
html = http.fetch_html(config.url) html = http.fetch_html(config.url)

View File

@@ -1,10 +1,10 @@
import re import re
from common import dates, endoflife, http, releasedata from common import dates, http, releasedata
DATE_PATTERN = re.compile(r"\d{4}-\d{2}-\d{2}") DATE_PATTERN = re.compile(r"\d{4}-\d{2}-\d{2}")
for config in endoflife.list_configs_from_argv(): for config in releasedata.list_configs_from_argv():
with releasedata.ProductData(config.product) as product_data: with releasedata.ProductData(config.product) as product_data:
wikicode = http.fetch_markdown(config.url) wikicode = http.fetch_markdown(config.url)

View File

@@ -1,7 +1,7 @@
import logging import logging
import re import re
from common import dates, endoflife, http, releasedata from common import dates, http, releasedata
"""Fetches Veeam products versions from https://www.veeam.com. """Fetches Veeam products versions from https://www.veeam.com.
@@ -9,7 +9,7 @@ This script takes a single argument which is the url of the versions page on htt
such as `https://www.veeam.com/kb2680`. such as `https://www.veeam.com/kb2680`.
""" """
for config in endoflife.list_configs_from_argv(): for config in releasedata.list_configs_from_argv():
with releasedata.ProductData(config.product) as product_data: with releasedata.ProductData(config.product) as product_data:
html = http.fetch_html(config.url) html = http.fetch_html(config.url)

View File

@@ -1,13 +1,13 @@
import logging import logging
import re import re
from common import dates, endoflife, http, releasedata from common import dates, http, releasedata
"""Fetches releases from VirtualBox download page.""" """Fetches releases from VirtualBox download page."""
EOL_REGEX = re.compile(r"^\(no longer supported, support ended (?P<value>\d{4}/\d{2})\)$") EOL_REGEX = re.compile(r"^\(no longer supported, support ended (?P<value>\d{4}/\d{2})\)$")
for config in endoflife.list_configs_from_argv(): for config in releasedata.list_configs_from_argv():
with releasedata.ProductData(config.product) as product_data: with releasedata.ProductData(config.product) as product_data:
html = http.fetch_html(config.url) html = http.fetch_html(config.url)

View File

@@ -1,6 +1,6 @@
from common import dates, endoflife, http, releasedata from common import dates, http, releasedata
for config in endoflife.list_configs_from_argv(): for config in releasedata.list_configs_from_argv():
with releasedata.ProductData(config.product) as product_data: with releasedata.ProductData(config.product) as product_data:
html = http.fetch_html(config.url) html = http.fetch_html(config.url)

View File

@@ -1,3 +1,4 @@
import argparse
import json import json
import logging import logging
import subprocess import subprocess
@@ -9,9 +10,7 @@ from deepdiff import DeepDiff
from src.common.endoflife import AutoConfig, ProductFrontmatter, list_products from src.common.endoflife import AutoConfig, ProductFrontmatter, list_products
from src.common.gha import GitHubGroup, GitHubOutput, GitHubStepSummary from src.common.gha import GitHubGroup, GitHubOutput, GitHubStepSummary
from src.common.releasedata import DATA_DIR, SRC_DIR
SRC_DIR = Path('src')
DATA_DIR = Path('releases')
class ScriptExecutionSummary: class ScriptExecutionSummary:
@@ -66,7 +65,7 @@ def install_playwright() -> None:
def __delete_data(product: ProductFrontmatter) -> None: def __delete_data(product: ProductFrontmatter) -> None:
release_data_path = DATA_DIR / f"{product.name}.json" release_data_path = Path(__file__).resolve().parent / DATA_DIR / f"{product.name}.json"
if not release_data_path.exists() or product.is_auto_update_cumulative(): if not release_data_path.exists() or product.is_auto_update_cumulative():
return return
@@ -75,19 +74,23 @@ def __delete_data(product: ProductFrontmatter) -> None:
def __revert_data(product: ProductFrontmatter) -> None: def __revert_data(product: ProductFrontmatter) -> None:
release_data_path = DATA_DIR / f"{product.name}.json" release_data_path = Path(__file__).resolve().parent / DATA_DIR / f"{product.name}.json"
# check=False because the command fails if the file did not exist before # check=False because the command fails if the file did not exist before
subprocess.run(f'git checkout HEAD -- {release_data_path}', timeout=10, check=False, shell=True) subprocess.run(f'git checkout HEAD -- {release_data_path}', timeout=10, check=False, shell=True)
logging.warning(f"reverted changes in {release_data_path}") logging.warning(f"reverted changes in {release_data_path}")
def __run_script(product: ProductFrontmatter, config: AutoConfig, summary: ScriptExecutionSummary) -> bool: def __run_script(product: ProductFrontmatter, config: AutoConfig, summary: ScriptExecutionSummary) -> bool:
script = SRC_DIR / config.script script = Path(__file__).resolve().parent / SRC_DIR / config.script
logging.info(f"start running {script} for {config}") logging.info(f"start running {script} for {config}")
start = time.perf_counter() start = time.perf_counter()
# timeout is handled in child scripts # timeout is handled in child scripts
child = subprocess.run([sys.executable, script, config.product, str(config.method), str(config.url)]) script_args = [sys.executable, script, "-p", product.path, "-m", str(config.method), "-u", str(config.url)]
script_args = script_args + ["-v"] if logging.getLogger().isEnabledFor(logging.DEBUG) else script_args
child = subprocess.run(script_args)
success = child.returncode == 0 success = child.returncode == 0
elapsed_seconds = time.perf_counter() - start elapsed_seconds = time.perf_counter() - start
@@ -98,13 +101,10 @@ def __run_script(product: ProductFrontmatter, config: AutoConfig, summary: Scrip
return success return success
def run_scripts(summary: GitHubStepSummary, product_filter: str) -> bool: def run_scripts(summary: GitHubStepSummary, products: list[ProductFrontmatter]) -> bool:
exec_summary = ScriptExecutionSummary() exec_summary = ScriptExecutionSummary()
with GitHubGroup("Load Product Data"): for product in products:
product_list = list_products(product_filter)
for product in product_list:
if not product.has_auto_configs(): if not product.has_auto_configs():
continue continue
@@ -166,24 +166,31 @@ def generate_commit_message(old_content: dict[Path, dict], new_content: dict[Pat
commit_message.println("") commit_message.println("")
summary.println("") summary.println("")
if __name__ == "__main__":
parser = argparse.ArgumentParser(description='Update product releases.')
parser.add_argument('product', nargs='?', help='restrict update to the given product')
parser.add_argument('-p', '--product-dir', required=True, help='path to the product directory')
parser.add_argument('-v', '--verbose', action='store_true', help='enable verbose logging')
args = parser.parse_args()
logging.basicConfig(format="%(message)s", level=logging.INFO) logging.basicConfig(format=logging.BASIC_FORMAT, level=(logging.DEBUG if args.verbose else logging.INFO))
p_filter = sys.argv[1] if len(sys.argv) > 1 else None
with GitHubStepSummary() as step_summary:
install_playwright() install_playwright()
some_script_failed = run_scripts(step_summary, p_filter)
updated_products = get_updated_products()
step_summary.println("## Update summary\n") products_dir = Path(args.product_dir)
if updated_products: products_list = list_products(products_dir, args.product)
new_files_content = load_products_json(updated_products)
subprocess.run('git stash --all --quiet', timeout=10, check=True, shell=True)
old_files_content = load_products_json(updated_products)
subprocess.run('git stash pop --quiet', timeout=10, check=True, shell=True)
generate_commit_message(old_files_content, new_files_content, step_summary)
else:
step_summary.println("No update")
sys.exit(1 if some_script_failed else 0) with GitHubStepSummary() as step_summary:
some_script_failed = run_scripts(step_summary, products_list)
updated_products = get_updated_products()
step_summary.println("## Update summary\n")
if updated_products:
new_files_content = load_products_json(updated_products)
subprocess.run('git stash --all --quiet', timeout=10, check=True, shell=True)
old_files_content = load_products_json(updated_products)
subprocess.run('git stash pop --quiet', timeout=10, check=True, shell=True)
generate_commit_message(old_files_content, new_files_content, step_summary)
else:
step_summary.println("No update")
sys.exit(1 if some_script_failed else 0)