🦥

Jenkins の Pipeline script を GitHub で管理して zx で対話形式に更新する

2023/04/05に公開

Jenkins の Pipeline Script の更新って大変ですよね…

UI からやろうって日にはもう大変😩
広げることのできないテキストエリアに、別途エディタで書いたものをコピペするという無駄ムーブ、のくせに事故があると困るものなので更新時には要ダブルチェック

なら GitHub 管理している .groovy が更新されたら Actions が動いて勝手に更新されるようにしちゃいましょう、ということで、まずはベースとなる手元で実行できるものを zxjenkins-cli を利用して作成しました💪

対象 Jenkins サーバー向けの cli は UI の「Jenkinsの管理 > Jenkins CLI」から、jenkins-cli.jar をダウンロードできます

以下を利用するので npm で一括インストールします

npm i zx xml-js process minimist

必要ファイルを揃えたら、以下のように実行します

zx --quiet UpdateJenkinsPipeline.mjs --env staging

Jenkins ジョブのデータを引っ張ってくると xml なので、扱いやすくするために Json にコンバートして、script 部分を上書きしたら xml に戻して、アップデートを投げつけるという流れになっています

#!/usr/bin/env zx
import fs from "fs";
import convert from "xml-js";
import { argv } from "process";
import min from "minimist";

const ENV = min(argv).env;
const CONFIG = {
  ENV: ENV, // 対象環境 ex) staging, production
  CLI: ENV === "staging" 
    ? 'jenkins-cli-stg.jar' 
    : 'jenkins-cli-prd.jar',  // 対象環境の CLI
  URL: ENV === "staging" 
    ? 'https://staging/jenkins/'
    : 'https://production/jenkins/', // 対象の Jenkins サーバー URL
  VIEW: '', // 対象の view 名
}

// Jenkins 上のジョブ名と .groovy 名を一致させるようにします
const GROOVY = {
  groovy_name1: 'jenkins_job_name1',
  groovy_name2: 'jenkins_job_name2',
  groovy_name3: 'jenkins_job_name3'
}

// 一時ディレクトリを作成する
const TEMPDIR = await $`mktemp -d`;

// 指定 view のジョブ一覧を表示する
const showAllJobsInTargetView = async(view, name, pass) => {
  console.log(chalk.bgYellow(`${CONFIG.VIEW} のジョブ一覧`));
  const jobs = await $`java -jar ${CONFIG.CLI} -s ${CONFIG.URL} -auth ${name}:${pass} list-jobs ${view}`;
  console.log(jobs);
}

// ローカルのブランチ一覧を表示する
const showAllLocalBranches = async() => {
  console.log(chalk.bgYellow(`ローカルのブランチ一覧`));
  const branch = await $`git branch`;
  console.log(branch);
}

// 指定ジョブの script を指定 groovy データで上書きする
const updatePipelineScript = async(job, name, pass) => {
  // git 管理上の groovy データを取得する
  const newData = readGroovyFile(job);
  // Jenkins 上の xml データを取得し、Json にしてから新しいデータに書き換えて、また xml に戻す
  let json = JSON.parse(convert.xml2json(String(await $`java -jar ${CONFIG.CLI} -s ${CONFIG.URL} -auth ${name}:${pass} get-job ${job}`)));
  // 再帰して script を書き換える
  const updateData = await recursiveUpdateJobData(json, newData);
  // xml に戻してファイル化する
  await $`echo ${convert.json2xml(updateData, {compact: false, spaces: 2})} >> ${TEMPDIR}/temp.xml`;
  // 戻した xml ファイルでアップデートする
  await $`java -jar ${CONFIG.CLI} -s ${CONFIG.URL} -auth ${name}:${pass} update-job ${job} < ${TEMPDIR}/temp.xml`;
}

// 指定 groovy データを読み込む
const readGroovyFile = (job) => {
  return fs.readFileSync(`./groovy_dir/${CONFIG.ENV}/${GROOVY[job]}.groovy`, { encoding: 'utf-8' });
}

// 再帰で json 内を検索して書き換える
const recursiveUpdateJobData = async(json, newData) => {
  if (!json.elements) return;
  for (let ele of json.elements) {
    if (ele.name === 'script') {
      ele.elements.forEach((e) => {
        e.text = newData;
      })
    } 
    await recursiveUpdateJobData(ele, newData);
  }
  return json;
}

// TEMPDIR を削除する
const removeTempXml = async() => {
  return $`rm -r ${TEMPDIR}`;
}

// ブランチを切り替える
const checkoutTargetBranch = async(branch) => {
  console.log('');
  console.log(`ブランチ切り替えて、更新します…`);
  await $`git checkout ${branch}`;
  await $`git pull`;
  return;
};

// 現在のブランチ名を取得する
const getCurrentBranchName = async() => {
  let currentBranch = await $`git rev-parse --abbrev-ref HEAD`;
  currentBranch = currentBranch.stdout.replace(/\n/g, '');
  return currentBranch;
}

// ローカルのブランチ一覧を取得する
const getBranchList = async() => {
  let branchList = await $`git branch`;
  // ブランチ一覧を配列化して整える
  branchList = branchList.stdout.split('\n').map(name => {
    return name.replace(/  /g, '').replace(/\* /g, '');
  }).filter((branch) => branch !== '');

  return branchList;
}

// 存在するブランチ名が指定されるまで確認を続けて、ブランチ名を取得する
const getBranchName = async() => {
  const branchList = await getBranchList();
  let branchName;
  do {
    branchName = await question('対象ブランチ名は? (ex: feature/hoge (空エンターで現在のブランチ名が入ります)):');
    // 現在いるブランチをデフォルトブランチにし、指定があれば指定にする
    branchName = branchName ? branchName : await getCurrentBranchName();
    if (!branchList.includes(branchName)) {
      console.log(chalk.bgRed('存在しないブランチ名を指定しています!'));
      console.log('');
    }
  } while (!branchList.includes(branchName))

  return branchName;
}

// 存在するジョブ名が指定されるまで確認を続けて、ジョブ名を取得する
const getTargetJobName = async() => {
  let jobName;
  do {
    jobName = await question('アップデートしたいジョブ名は?');
    if (!GROOVY.hasOwnProperty(jobName)) {
      console.log(chalk.bgRed('存在しないジョブ名を指定しています!'));
      console.log('');
    }
  } while (!GROOVY.hasOwnProperty(jobName))

  return jobName;
}

// 対話形式で実行する
try {
  const name = await question('ログイン用の ID を入力してください…:');
  const pass = await question('ログイン用のパスワードを入力してください…:');
  console.log('');
  await showAllJobsInTargetView(CONFIG.VIEW, name, pass);
  console.log('');
  const job = await getTargetJobName();
  console.log('');
  await showAllLocalBranches();
  console.log('');
  const branch = await getBranchName();
  await checkoutTargetBranch(branch);
  await updatePipelineScript(job, name, pass);
} finaly {
  await removeTempXml();
}

console.log('アップデート完了しました!');

手元から更新できるのを確認したら、あとは Actions 用に手直しをして、自動化していきましょう
よき zx ライフを!

Discussion