endoflife.dateからEOL情報を取得しGoogleスプレッドシートで一覧化する

アノテーション株式会社のあのふじたです。
最近、様々なプロダクトごとのEOL情報をまとめたサイト「endoflife.date」 を知りました。
このサイトのAPIを利用して、EOL情報を取得し、Googleスプレッドシートで一覧化してみたいと思います。

endoflife.date とは

endoflife.date は、様々な製品のEOL日付とサポートライフサイクルを文書化しているサイトです。

API のページ

APIの詳細は以下のリンクから確認できます。
API ドキュメント 現時点では3つのエンドポイントが説明されています。

  • Get All Details
  • Single cycle details
  • All Products

All Products エンドポイントを使うと、全プロダクトの情報が取得できます。
全プロダクト情報
Get All Details は特定のプロダクトの全情報を
Single cycle details は特定のプロダクトの1つのリリースサイクルの情報を取得できます。

スプレッドシートの構成

全てのプロダクト一覧を取得するのは All Products エンドポイントで一発ですが、現在 309 プロダクトをトラックしているようです。必要なプロダクトのみに絞ってEOL情報を取得したいですね。
以下のようなシート構成にします。

  1. Productsシート: All Productsリクエストで取得したプロダクト一覧を記載
  2. GetProductsListシート: トラックしたいプロダクトを選別
  3. EOLシート: GetProductsListシートのプロダクト一覧を元に、Get All Detailsを複数回実行しEOL情報を記載
  4. Logsシート: 定期実行を前提に実行ログを記載
  5. Diffシート: 更新があった際の差分を記載

1. プロダクト一覧をシートに記載するスクリプト

まずは All Products へのリクエスト結果をスプレッドシートに記載するGoogle App Scriptを作成します。
拡張機能 > App Script からプロジェクトを立ち上げ、コード.gsgetAllProducts.gs に書き換えて以下のコードを記載します。

function writeToProdutsSheet() {
    const ss = SpreadsheetApp.getActiveSpreadsheet();
    const sheet = ss.getSheetByName('Products') || ss.insertSheet('Products');

  const url = 'https://endoflife.date/api/all.json';
  const options = {
    'method': 'GET',
    'headers': {'Accept': 'application/json'},
    'muteHttpExceptions': true // 例外をミュートにする
  };

  // データを取得してパースします
  let response, data;
  try {
    response = UrlFetchApp.fetch(url, options);
    // 正常なレスポンスを確認します
    if (response.getResponseCode() === 200) {
      data = JSON.parse(response.getContentText());
      // スプレッドシートに書き込む前にデータを加工します
      const dataArray = data.map((item, index) => [index + 1, item]);
      // スプレッドシートの A1 セルからデータを書き込みます
      const range = sheet.getRange(1, 1, dataArray.length, 2); // 行数と列数を指定します
      range.setValues(dataArray);
    } else {
      throw new Error('非正常なレスポンス: ' + response.getResponseCode());
    }
  } catch (error) {
    Logger.log(error.message);
  }
}

実行を押すとアクセス権限のポップアップがでますが、承認し実行します。

シート[Products] で有効化したいプロダクトのC列に enable を入力しておきます。入力規則で enable を入力しやすくしておくのも良いかと思います。

今回は amazon と aws が含まれたプロダクトのみにします。

2. トラックしたいプロダクトのみに選別するシートの設定

GetProductListシートのA1にquery関数でProductsシートでenableとなった行のみ抽出します。
A1=query(Products!A1:C500,"select A, B, C where C='enable'") と入力します。

以下のように抽出されます

  • amazon-cdk
  • amazon-corretto
  • amazon-eks
  • amazon-glue
  • amazon-linux
  • amazon-neptune
  • amazon-rds-mysql
  • amazon-rds-postgresql
  • aws-lambda

3. GetProductListシートからプロダクト一覧を取得し各プロダクトのEOL情報や差分をシートに記載する

EOL情報を記載する GoogleAppScript を作成します。


+をクリックしてスクリプトを追加し、コード.gsgetGetAllDetails.gs に書き換えて以下のコードを記載します。

function writeToEolSheet() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const eol_sheet = ss.getSheetByName('EOL');
  const eoldiff_sheet = ss.getSheetByName('Diff') || ss.insertSheet('Diff'); // 差分シートがなければ作成
  const logs_sheet = ss.getSheetByName('Logs') || ss.insertSheet('Logs'); // ログシートがなければ作成
  const headers = ["Products", "Cycle", "Release Date", "EOL", "Latest", "latestReleaseDate", "LTS", "Support", "Discontinued", "Link"];

  // 現在のデータを一時的に保存
  const currentDataRange = eol_sheet.getDataRange();
  const currentData = currentDataRange.getValues();

  // データのみをクリア(フォーマットは保持)
  eol_sheet.clearContents();
  eol_sheet.appendRow(headers); // ヘッダーの追加

  const sheet = ss.getSheetByName('GetProductsList');
  const nameColumnIndex = 2;
  const lastRow = sheet.getLastRow();
  const productNamesRange = sheet.getRange(1, nameColumnIndex, lastRow - 1);
  const productNames = productNamesRange.getValues(); // 一括でプロダクト名を取得

  const dataToWrite = [];

  // プロダクト名毎にAPIから情報を取得
  productNames.forEach(function(row, index) {
    const productName = row[0].trim();
    if (productName) { // プロダクト名が空でない場合のみ処理
      const url = 'https://endoflife.date/api/' + encodeURIComponent(productName) + '.json';
      const options = {
        method: 'get',
        muteHttpExceptions: true
      };

      try {
        const response = UrlFetchApp.fetch(url, options);
        const statusCode = response.getResponseCode();
        if (statusCode === 200) {
          var data = JSON.parse(response.getContentText());
          data.forEach(function(entry) {
            dataToWrite.push([
              productName,
              entry.cycle || '',
              entry.releaseDate || '',
              entry.eol || '',
              entry.latest || '',
              entry.latestReleaseDate || '',
              entry.lts || false,
              entry.support || '',
              entry.discontinued || '',
              entry.link || ''
            ]);
          });
        } else {
          console.error(`Product ${productName}: Failed to fetch data with status code ${statusCode}`);
        }
      } catch (e) {
        console.error(`Unable to fetch data for ${productName}: ${e}`);
      }
      // 待機を挿入(最後のリクエストの後は不要)
      if (index < productNames.length - 1) {
        Utilities.sleep(500);
      }
    }
  });

  // 一括でデータを書き込む
  if (dataToWrite.length > 0) {
    eol_sheet.getRange(eol_sheet.getLastRow() + 1, 1, dataToWrite.length, headers.length).setValues(dataToWrite);
  }

  // 差分の計算と出力
  const newDataRange = eol_sheet.getDataRange();
  const newData = newDataRange.getValues();
  const diffData = calculateEolDiff(currentData, newData);
  if (diffData.length > 0) {
    eoldiff_sheet.clearContents(); // 差分シートをクリア
    eoldiff_sheet.getRange(1, 1, diffData.length, diffData[0].length).setValues(diffData); // 差分を出力

    // ログシートに実行時間とメッセージを記載
    const timestamp = new Date();
    const logMessage = `Script executed at ${timestamp.toLocaleString()}: Updates found in EOL sheet.`;
    logs_sheet.appendRow([logMessage]);
  } else { 
    // ログシートに実行時間とメッセージを記載
    const timestamp = new Date();
    const logMessage = `Script executed at ${timestamp.toLocaleString()}: No Updates in EOL sheet.`;
    logs_sheet.appendRow([logMessage]);
  }

  // 列の幅を整列
  eol_sheet.autoResizeColumns(1, headers.length);
}

// 差分を計算する関数
function calculateEolDiff(oldData, newData) {
  const diff = [];
  const oldDataStringified = oldData.map(row => JSON.stringify(row));
  const newDataStringified = newData.map(row => JSON.stringify(row));

  // 新しいデータをチェックし、古いデータに存在しない行を検出
  newDataStringified.forEach((newRow, i) => {
    if (!oldDataStringified.includes(newRow)) {
      diff.push(['+', ...newData[i]]); // 新規追加された行に "+" を追加
    }
  });

  // 古いデータをチェックし、新しいデータに存在しない行を検出
  oldDataStringified.forEach((oldRow, i) => {
    if (!newDataStringified.includes(oldRow)) {
      diff.push(['-', ...oldData[i]]); // 削除された行に "-" を追加
    }
  });

  return diff;
}

処理の流れは以下の通りです。

  1. 現在のEOLシートのデータを一時的に保存
  2. データのみをクリア(フォーマットは保持)
  3. GetProductListシートから一括でプロダクト名を取得
  4. プロダクト名毎にAPIから情報を取得(少し待機を挿入)
  5. EOLシートに一括でデータを書き込む
  6. 差分があるか確認し、DiffシートとLogsシートに書き込む

実行後のEOLシートの状況です。

Diffシートは見やすいように条件付き書式A列の+-入力に応じて色を付けると良いでしょう。

まとめ

任意のプロダクトのEOLの一覧ページがざっくりと作成できました。 getGetAllDetails.gs を定期実行し、Slackなどに通知できるようにすれば、利用できる場面もあるのではないでしょうか。

アノテーション株式会社について

アノテーション株式会社はクラスメソッドグループのオペレーション専門特化企業です。
サポート・運用・開発保守・情シス・バックオフィスの専門チームが、最新 IT テクノロジー、高い技術力、蓄積されたノウハウをフル活用し、お客様の課題解決を行っています。
当社は様々な職種でメンバーを募集しています。「オペレーション・エクセレンス」と「らしく働く、らしく生きる」を共に実現するカルチャー・しくみ・働き方にご興味がある方は、アノテーション株式会社 採用サイトをぜひご覧ください。