GitHub Actions から FTP 転送する
自分のメインサイト Neo's World は GitHub で管理しているが、ファイルの FTP 転送は手動で行っていた。
一応 npm スクリプトを用意してコマンドラインから FTP 転送したりできるようにはしていたが、完全自動化したくなり、GitHub Actions でコネコネすることにした。
- ftp-client で指定のファイルを FTP アップロードする
- ↑ 今回も使用する ftp-client の解説。
- ftp-deploy で指定のディレクトリを FTP アップロードする
目次
GitHub Actions Workflow
今回作成した GitHub Actions Workflow は次のとおり。
.github/workflows/ftp.yaml
name: FTP
on:
push:
branches:
- master
workflow_dispatch:
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Use Node.js
uses: actions/setup-node@v1
with:
node-version: 12.x
# 変更があったファイルを JSON ファイルに書き出す
- name: Get Changed Files
id : get_changed_files
uses: jitterbit/get-changed-files@v1
with:
format: json
- name: Write Changed Files
run : |
mkdir -p ./temp/
echo '${{ steps.get_changed_files.outputs.added_modified }}' > ./temp/added_modified.json
echo '${{ steps.get_changed_files.outputs.renamed }}' > ./temp/renamed.json
echo '${{ steps.get_changed_files.outputs.removed }}' > ./temp/removed.json
cat ./temp/added_modified.json
cat ./temp/renamed.json
cat ./temp/removed.json
# JSON ファイルを基にアップロードするファイル名の配列を組み立て ./temp/upload-files.json に書き出す
- name: Detect Upload Files
id : detect_upload_files
run : |
node ./.github/workflows/ftp-detect-upload-files.js
if [[ -e ./temp/upload-files.json ]]; then
echo "::set-output name=do_upload::true"
echo 'Do Upload'
else
echo "::set-output name=do_upload::false"
echo 'Do Not Upload'
fi
# 以降のデプロイ処理は do_upload Output が true の時だけ実行する
- name: Install
if : steps.detect_upload_files.outputs.do_upload == 'true'
run : npm install
- name: Build
if : steps.detect_upload_files.outputs.do_upload == 'true'
run : npm run build --if-present
- name: FTP Upload Files
if : steps.detect_upload_files.outputs.do_upload == 'true'
run : node ./.github/workflows/ftp-upload-files.js
env :
FTP_PASSWORD: ${{ secrets.FTP_PASSWORD }}
- master ブランチへの Push 時に起動する
- Get Changed Files
jitterbit/get-changed-files@v1
Action を使い、コミットを参照して変更が発生したファイルを JSON 形式で取得する- JSON の形式は次のような配列形式になる
- 対象ファイルが1件もない場合 :
[]
← 空配列 - 対象ファイルがある場合 :
["src/pages/index.html","src/styles/index.scss"]
といった感じ
- 対象ファイルが1件もない場合 :
- Write Changed Files
- その内容を JSON ファイルに書き出しておく
- 前述のとおり、対象ファイルがなくても空配列の文字列
[]
が書き込まれるので、後続の Node.js スクリプト内でrequire()
を使って読み込んでもエラーにならない
- Detect Upload Files
- その JSON ファイルを読み取って、FTP アップロードすべきファイル名を列挙した JSON ファイルを作る
ftp-detect-upload-files.js
という自作の Node.js スクリプトを動かす (後述)- 結果ファイルの有無を基に、アップロード処理が必要かどうかを Output に出力しておく
- アップロード処理が必要なら、
npm install
→npm run build
実行後、FTP アップロードを行う - FTP Upload Files
- FTP アップロード処理は、
ftp-client
パッケージを使った自作の Node.js スクリプトftp-upload-files.js
で行っている - FTP パスワードを Secret で渡している
- FTP アップロード処理は、
という感じ。
jitterbit/get-changed-files
が要で、その後のファイル調整や FTP アップロード処理は自前でゴリゴリ実装している。
アップロードが必要なファイル名を列挙するスクリプト
「Detect Upload Files」Step で動かしている ftp-detect-upload-files.js
の内容はこんな感じ。
./.github/workflows/ftp-detect-upload-files.js
const fs = require('fs');
const srcDir = 'src/';
const distDir = 'docs/';
// 前 Step で JSON ファイルに書き出しておいた変更ファイル一覧を取得する
const addedModified = require('../../temp/added_modified.json');
const renamed = require('../../temp/renamed.json');
const removed = require('../../temp/removed.json'); // 削除されたファイルがあるかどうかの確認だけ
if(removed.length) {
console.log('Removed Files Exist. Please Remove Manually');
}
const joined = [...addedModified, ...renamed];
// src/templates/ 配下の変更がある場合は一切のアップロードを行わないこととする
const isTemplateChanged = joined.some((file) => file.includes(srcDir + 'templates/'));
if(isTemplateChanged) {
console.error('Templates Changed! Please Upload Manually. Aborted');
return process.exit(1);
}
// src/ 配下の変更ファイルのみ抽出する
const filtered = joined.filter((file) => file.includes(srcDir));
// ビルド後のコンテンツに対応するようファイル名を直す
const uploadFiles = filtered.map((file) => {
if(file.includes(srcDir + 'scripts/')) {
return distDir + 'scripts.js';
}
if(file.includes(srcDir + 'styles/')) {
return distDir + 'styles.js';
}
if(file.includes(srcDir + 'pages/')) {
return file.replace(srcDir + 'pages/', distDir);
}
// 知らないパスにファイルが登場したら中止する
throw new Error(`The file path is not supported : [${file}]`);
});
// 変更ファイルがなければ結果ファイルを作らず終了する
if(!uploadFiles.length) {
return console.log('Upload Files Not Exist');
}
const stringified = JSON.stringify(uploadFiles);
console.log('Upload Files :');
console.log(stringified);
fs.writeFileSync('./temp/upload-files.json', stringified, 'utf-8');
- 前 Step で書き出しておいた JSON を
require()
で読み込んでいる
jitterbit/get-changed-files
は JSON で出力するので、ファイルに書き出したりしなくとも、jq
で読み込んで jq
芸をキメて、アップロードしたいファイルの列挙をしても良い。
今回はちょっと複雑なファイル名の置換処理があったので、JavaScript で処理してしまったが、.github/
ディレクトリだけはアップしたくない、ぐらいの Filter 処理なら、jq で処理して Output しても良いだろう。
- 「変更があったファイル名」は、
src/
から始まるソースコードとして特定されるが、FTP アップロードしたいのはnpm run build
で生成されるdist/
配下のファイルとなる - そこで、このスクリプト内で
filter()
したりmap()
したりして、アップロードしたいdist/
配下のファイル名を組み立て、JSON ファイルに書き出すのである - ファイル削除の変更には対応していないので、削除処理は手作業でやってね、というメッセージを出力している
- プロジェクト都合だが、全量再アップロードになる
src/templates/
配下の変更があった場合はエラーとし、自分でやってもらうようにしている。よって、変更の内容によっては完全自動で FTP 転送が完了するワケではない
アップロードしたいファイルが1件でもあれば、JSON ファイルを書き出しているので、コレをその後のシェルスクリプトでチェックして、後続処理を行うかどうかの判定用 Output を用意している。
if [[ -e ./temp/upload-files.json ]]; then
echo "::set-output name=do_upload::true"
echo 'Do Upload'
else
echo "::set-output name=do_upload::false"
echo 'Do Not Upload'
fi
以降の Step では、ココで Output した do_upload
フラグを if
で見ている。
- name: Install
if : steps.detect_upload_files.outputs.do_upload == 'true'
run : npm install
FTP アップロードするスクリプト
FTP アップロードするスクリプトは ftp-client
パッケージを利用している。
./.github/workflows/ftp-upload-files.js
const FtpClient = require('ftp-client');
// 手前の Node.js スクリプトが出力した JSON ファイルを読み込む
const uploadFiles = require('../../temp/upload-files.json');
const ftpClient = new FtpClient({
user : '【ユーザ名】',
password: process.env['FTP_PASSWORD'], // Secret からパスワードを注入する
host : '【サーバ名】'
}, {
logging: 'debug',
overwrite: 'all'
});
ftpClient.connect(() => {
console.log('Connected');
ftpClient.upload(
uploadFiles, // ← ココで配列として指定する
'/public_html/',
{
baseDir: 'docs',
overwrite: 'all'
},
(result) => {
if(result.errors && Object.keys(result.errors).length) {
console.error('Upload Error');
console.error(result);
throw new Error('Upload Error');
}
console.log('Uploaded')
console.log(result);
}
);
});
自分が送信対象としている XREA サーバの仕様に合わせて各種設定を入れている。FTP パスワードなどのクレデンシャル情報は Secret を環境変数に注入して使用すること。
以上
FTP 転送ができる GitHub Actions もあるのだが、自分の XREA サーバに対して上手く動かせなかったので、結局自前でゴリゴリ実装していった。
ファイル削除に対応しきれていなかったりはするが、とりあえずやりたかったことは上手くできたのでコレで OK。