CircleCI や GitHub Actions の cron を祝日だけ停止させたい
先日の ua-parse-js のハイジャックの件 を受けて、業務の中で毎日動かしている On-premise Renovate の cron を土日祝に停止させたいという話が上がった。
業務の合間に書く時間がちょっと捻出できそうになかったこと、加えて汎用的なコードということもあり、プライベートでも使えそうだったので一般化した範囲でコードを書いてしまって、業務で社内用に調整する形で決着させたので、せっかくなので共有しておく。
社内が基本的に CircleCI なので特化したものと、一般的に使えるものでバリエーションごとに2つのパターンを用意した。
祝日に停止させるアプローチ
ひとまず今回は内製の Bot の運用のため、以下のような特徴があった。
- 土日の設定自体は cron で曜日指定ができるため祝日にフォーカスして良い
- 厳密性を重視しない
- ミッションクリティカルな領域の話ではない
以上を考えると、コスト対効果が十分であることが望ましいと考え、自力で国のデータや Google のカレンダーを参照する形ではなく、もっとライトな Holidays JP API を活用することにした。
理由としては Renovate のレポジトリ自体が設定ファイルを含めて JS で記述していることから、祝日チェッカーを動作させるには Node.js 環境が適切であること、そして JSON 形式であれば、 JavaScript で取り回しやすく、組織ごとの休暇データと合成できることからとなる。
汎用版祝日チェッカー
以上を踏まえて作ったのがこれ。
実行環境をできるだけ問わないようにしたいため、 CommonJS かつ JavaScript で仕上げた。
/**
* checkHoliday.js
* https://d.potato4d.me/entry/20211102-circleci-cron-holiday
*/
const axios = require('axios')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const timezone = require('dayjs/plugin/timezone')
const customHolidays = require('./customHolidays')
const { execSync } = require('child_process')
dayjs.extend(utc)
dayjs.extend(timezone)
async function run() {
try {
const { data } = await axios.get('https://holidays-jp.github.io/api/v1/date.json')
const holidays = [
...Object.keys(data),
customHolidays
]
if (holidays.includes(dayjs().tz('Asia/Tokyo').format('YYYY-MM-DD'))) {
console.log('Today is holiday.')
process.exit(1)
} else {
console.log('Today is not holiday.')
process.exit(0)
}
} catch(e) {
console.error(e)
process.exit(1)
}
}
run();
customHolidays は .js
でも .json
でも良いので、好きな休日データを以下のように記述すると良い。
例えば三ヶ日は暦上は元旦のみ休みであるが、一般的には三ヶ日はすべて祝日である。
module.exports = [
'2021-01-02',
'2021-01-03',
]
基本的に平日であれば実行、休日であれば実行を中断したいため、祝日はもちろん、エラー落ちの場合も平日という確証が得られないため exit code 1 を返すように処理した。
より汎用的な作りとしているため、実用する場合は CircleCI だと when を使って on_fail でキャッチしたり、 GitHub Action の場合は if で直前の step の exit code を参照できるため、そちらでキャッチすると良い。
CircleCI 特化型祝日チェッカー
と、上記で汎用的な作りにしたものの、ただ cron を動かすためだけに CircleCI 側の知識を色々と必要とするのも微妙だと感じたため、実際に今稼働させているものはもう少し愚直なものとした。
/**
* checkHoliday.js
* https://d.potato4d.me/entry/20211102-circleci-cron-holiday
*/
const axios = require('axios')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const timezone = require('dayjs/plugin/timezone')
const customHolidays = require('./modules/customHolidays')
const { execSync } = require('child_process')
const { CI } = process.env
dayjs.extend(utc)
dayjs.extend(timezone)
async function run() {
try {
const { data } = await axios.get('https://holidays-jp.github.io/api/v1/date.json')
const holidays = [
...Object.keys(data),
customHolidays
]
if (holidays.includes(dayjs().tz('Asia/Tokyo').format('YYYY-MM-DD'))) {
console.log('Today is holiday.')
if (CI) {
execSync('circleci-agent step halt')
} else {
process.exit(1)
}
} else {
console.log('Today is not holiday.')
process.exit(0)
}
} catch(e) {
console.error(e)
process.exit(1)
}
}
run();
environment variable CI
がある場合、それが祝日の場合は circleci-agent step halt
を実行、ローカルマシンでも CI の状況を再現できるようにしつつ、通常のローカルでの開発時は exit code を見るだけで済むようにした。
終わりに
プロダクションに乗るコードに対して OSS を使うことは頻繁にあるにも関わらず、社内ツールの設定ひとつにしか影響しない API を選ぶときは、国のオフィシャルじゃないと多少不安になってしまう。
おそらくは成果物としてビルドされていたり、一度インストールしたあとの node_modules は基本的に内容が変わることはないが、API は都度 call するため、変わらないという保証がないことに起因すると考えているものの、それはそれで変な話だなと感じたり。