※ これから記載する事項は、私が所属する会社とは一切関係のない事柄です。
この記事では前回紹介した Salesforce B2C Commerce sfcc-ciについて と Salesforce B2C Commerce カートリッジソースコードのテストについて を応用してCircleCIを利用し、子ブランチがstagingにマージされてからデプロイされるまでの流れを作ってみました。
今回目指す仕様
- stagingブランチへプッシュ(マージ)された時に実行される
- コンテナ内に依存関係のインストール
- ユニットテストの実行
- 静的リソースのビルド
- ソースコードのデプロイ
- ソースコードのアクティブ化
- インテグレーションテストの実行
- E2Eテストの実行
-
/cartridges
内にカートリッジを追加していく -
/sample_sitedata
にサイトにインポートするソースコード追加する - インテグレーション又はE2Eテストでエラーだった場合はソースコードはサーバーに残りますが、以前アクティブだったソースコードバージョンに戻します
今回取り扱わない内容
- PR前のコミット時のLintチェック
- feature/bugfixなどの子ブランチへプッシュ後のユニットテスト
- テストコードの内容。
- デプロイ前のstaging環境の確認
- コードのレプリケーション
- CircleCIの使い方
- プリビルドした image は使わないので実行に時間かかります
- 並列処理、キャッシュなどの高速化はしていません。高速したい場合の参考記事。
- キャッシュクリアや再インデックス作成
前提
- ローカルマシンとして、macOS Big Sur version 11.6.6 を利用しました
- sfcc-ciを利用するための設定はあらかじめ Salesforce B2C Commerce sfcc-ciについて を参照してください
- SFRAにデフォルトで入っているRefArchサイトを利用しました。
実装内容
1. SFRAのソースコードのコピー
SFRA のソースコードをローカルにコピーします。
2. スクリプトの設置
今回は下記の4つのスクリプトを作成しました。このスクリプトをソースコードのルートディレクトリに置きます。
- cicd-install.sh
- cicd-environment.sh
- cicd-deploy.sh
- cicd-test.sh
#!/bin/sh
set -eu
# set -x
## 依存関係をインストール
sudo apt-get update
sudo apt-get install -y libgtk-3.0 libgbm-dev libnss3 libatk-bridge2.0-0 libasound2
## sfcc-ci をインストール
wget https://github.com/SalesforceCommerceCloud/sfcc-ci/releases/download/v2.9.1/sfcc-ci-linux
chmod +x ./sfcc-ci-linux
sudo mv ./sfcc-ci-linux /usr/local/bin/sfcc-ci
## NPM パッケージをインストール
npm install
npm install webpack-cli -D
npx create-codeceptjs .
npm install --save-dev puppeteer
npm audit fix
#!/bin/sh
set -eu
# set -x
case $CIRCLE_BRANCH in
"staging")
export SFCC_HOST=${SFCC_HOST_STAGING}
export SFCC_OAUTH_CLIENT_ID=${SFCC_OAUTH_CLIENT_ID_STAGING}
export SFCC_OAUTH_CLIENT_SECRET=${SFCC_OAUTH_CLIENT_SECRET_STAGING}
;;
esac
# 環境変数にセットする
echo 'export SFCC_HOST='$SFCC_HOST >> $BASH_ENV
echo 'export SFCC_OAUTH_CLIENT_ID='$SFCC_OAUTH_CLIENT_ID >> $BASH_ENV
echo 'export SFCC_OAUTH_CLIENT_SECRET='$SFCC_OAUTH_CLIENT_SECRET >> $BASH_ENV
#!/bin/sh
set -eu
# set -x
if [ -z "$SFCC_HOST" ] || [ -z "$SFCC_OAUTH_CLIENT_ID"] || [ -z "$SFCC_OAUTH_CLIENT_SECRET" ]; then
echo "No credential or host found."
exit 1
fi
# シェルのあるディレクトリパスの取得
THIS_FILE_DIR=$(cd $(dirname $0); pwd)
# カートリッジとサイトデータのディレクトリ名の定義
CARTRIDGE_DIR_NAME="cartridges"
SITEDATA_DIR_NAME="sample_sitedata"
# カートリッジとサイトデータのディレクトリパスとバージョンの定義
CARTRIDGE_DIR=$THIS_FILE_DIR"/"$CARTRIDGE_DIR_NAME
SITEDATA_DIR=$THIS_FILE_DIR"/"$SITEDATA_DIR_NAME
NEW_VERSION="version_"`date +'%Y%m%d%H%M%S'`
# 一時ディレクトリの定義と作成
TMP_DIR=$THIS_FILE_DIR"/build"
mkdir -p $TMP_DIR
# 静的リソースのビルド
npx webpack
# 一時ディレクトリにソースを移動
cp -rf $CARTRIDGE_DIR $TMP_DIR
cp -rf $SITEDATA_DIR $TMP_DIR
# 一時ディレクトリでzip圧縮
cd $TMP_DIR
mv $CARTRIDGE_DIR_NAME $NEW_VERSION
zip -r $NEW_VERSION".zip" $NEW_VERSION
zip -r $SITEDATA_DIR_NAME".zip" $SITEDATA_DIR_NAME
# シェルのあるディレクトリパスに戻る
cd $THIS_FILE_DIR
# sfcc-ciのデバッグを利用
export DEBUG=true
# sfcc-ciの認証
sfcc-ci client:auth
# 現在最新のバージョンを取得
LATEST_VERSION=`sfcc-ci code:list -j -i $SFCC_HOST | jq -R 'fromjson?' | jq '[.data[]]' | jq 'map(select(.active==true))' | jq -r '.[0].id'`
# サイトデータのデプロイとソースコードのアクティブ化
sfcc-ci instance:upload $TMP_DIR"/"$SITEDATA_DIR_NAME".zip" -i $SFCC_HOST
sfcc-ci instance:import $SITEDATA_DIR_NAME".zip" -fjs -i $SFCC_HOST
# カートリッジのデプロイとソースコードのアクティブ化
sfcc-ci code:deploy $TMP_DIR"/"$NEW_VERSION".zip" -i $SFCC_HOST
sfcc-ci code:activate $NEW_VERSION -i $SFCC_HOST
# 一時ディレクトリの削除
rm -rf $TMP_DIR
# バージョンを環境変数にセットする
echo "=========新しいバージョン========="
echo $NEW_VERSION
echo "=========直近の最新バージョン========="
echo $LATEST_VERSION
echo 'export LATEST_VERSION='$LATEST_VERSION >> $BASH_ENV
echo 'export NEW_VERSION='$NEW_VERSION >> $BASH_ENV
#!/bin/sh
set -eu
# set -x
catch(){
if [ "$?" = 1 ]; then
echo "テストエラーのため、"$LATEST_VERSION" バージョンを再度アクティブにします"
sfcc-ci code:activate $LATEST_VERSION -i $SFCC_HOST
fi
}
trap catch EXIT
# インテグレーションテスト
npm run test:integration -- --baseUrl $SFCC_HOST
# E2Eテスト
npx codeceptjs run --verbose --grep @happyPath
3. Webpackの設定
下記コマンドで /cartridges
内のカートリッジのSCSSとJSファイルをビルドするための設定を行います。
npx webpack
-
npm install webpack-cli -D
を実行してwebpack コマンドを利用できるようにする -
webpack.conf.js
を下記のように書き換えます。
"use strict";
var path = require("path");
var webpack = require("sgmf-scripts").webpack;
var ExtractTextPlugin = require("sgmf-scripts")["extract-text-webpack-plugin"];
const glob = require("glob");
const cwd = process.cwd();
var bootstrapPackages = {
Alert: "exports-loader?Alert!bootstrap/js/src/alert",
// Button: 'exports-loader?Button!bootstrap/js/src/button',
Carousel: "exports-loader?Carousel!bootstrap/js/src/carousel",
Collapse: "exports-loader?Collapse!bootstrap/js/src/collapse",
// Dropdown: 'exports-loader?Dropdown!bootstrap/js/src/dropdown',
Modal: "exports-loader?Modal!bootstrap/js/src/modal",
// Popover: 'exports-loader?Popover!bootstrap/js/src/popover',
Scrollspy: "exports-loader?Scrollspy!bootstrap/js/src/scrollspy",
Tab: "exports-loader?Tab!bootstrap/js/src/tab",
// Tooltip: 'exports-loader?Tooltip!bootstrap/js/src/tooltip',
Util: "exports-loader?Util!bootstrap/js/src/util",
};
const idEmptyObj = (obj) => {
return !Object.keys(obj).length;
};
const createJsPath = (cartridgeName) => {
const result = {};
const jsFiles = glob.sync(
`./cartridges/${cartridgeName}/cartridge/client/default/js/*.js`
);
jsFiles.forEach((filePath) => {
let location = path.relative(
path.join(cwd, `./cartridges/${cartridgeName}/cartridge/client`),
filePath
);
location = location.substr(0, location.length - 3);
result[location] = filePath;
});
return result;
};
const constcreateScssPath = (cartridgeName) => {
const result = {};
const cssFiles = glob.sync(`./cartridges/${cartridgeName}/cartridge/client/default/scss/**/*.scss`);
cssFiles.forEach((filePath) => {
const name = path.basename(filePath, ".scss");
if (name.indexOf("_") !== 0) {
let location = path.relative(
path.join(cwd, `./cartridges/${cartridgeName}/cartridge/client`),
filePath
);
location = location.substr(0, location.length - 5).replace("scss", "css");
result[location] = filePath;
}
});
return result;
};
const getCSSConf = (cartridgeName) => {
const scssFiles = constcreateScssPath(cartridgeName);
if (idEmptyObj(scssFiles)) {
return;
}
return {
mode: "none",
name: "scss",
entry: scssFiles,
output: {
path: path.resolve(`./cartridges/${cartridgeName}/cartridge/static`),
filename: "[name].css",
},
module: {
rules: [
{
test: /\.scss$/,
use: ExtractTextPlugin.extract({
use: [
{
loader: "css-loader",
options: {
url: false,
minimize: true,
},
},
{
loader: "postcss-loader",
options: {
plugins: [require("autoprefixer")()],
},
},
{
loader: "sass-loader",
options: {
includePaths: [
path.resolve("node_modules"),
path.resolve("node_modules/flag-icon-css/sass"),
],
},
},
],
}),
},
],
},
plugins: [new ExtractTextPlugin({ filename: "[name].css" })],
};
};
const getJSConf = (cartridgeName) => {
const jsFiles = createJsPath(cartridgeName);
if (idEmptyObj(jsFiles)) {
return;
}
return {
mode: "production",
name: "js",
entry: jsFiles,
output: {
path: path.resolve(`./cartridges/${cartridgeName}/cartridge/static`),
filename: "[name].js",
},
module: {
rules: [
{
test: /bootstrap(.)*\.js$/,
use: {
loader: "babel-loader",
options: {
presets: ["@babel/env"],
plugins: ["@babel/plugin-proposal-object-rest-spread"],
cacheDirectory: true,
},
},
},
],
},
plugins: [new webpack.ProvidePlugin(bootstrapPackages)],
};
};
let configs = [];
var cartridgeDirs = glob.sync("cartridges/*/", {
matchBase: true,
ignore: "**/node_modules/**",
});
cartridgeDirs.forEach((file) => {
const cartridgeName = path.basename(file);
const jsConf = getJSConf(cartridgeName);
if (jsConf) {
configs.push(jsConf);
}
const scssConf = getCSSConf(cartridgeName)
if (scssConf) {
configs.push(scssConf);
}
});
module.exports = configs;
4. CircleCIの設定
/.circleci/config.yml
内に下記の内容を記載。
version: 2.1
executors:
my-executor:
docker:
- image: cimg/node:14.19.2
jobs:
install_test_deploy:
executor: my-executor
working_directory: ~/workspace
steps:
- checkout
- run:
name: Install
command: sh cicd-install.sh
- run:
name: Unit Test
command: npm test
- run:
name: Set environment
command: sh cicd-environment.sh
- run:
name: Deploy
command: sh cicd-deploy.sh
- run:
name: Integration and E2E Test
command: sh cicd-test.sh
workflows:
my-workflow:
jobs:
- install_test_deploy:
filters:
branches:
only:
- staging
5. codeceptjs の設定
ルートディレクトリの codecept.conf.js
に下記の内容を記載。
const { setHeadlessWhen, setCommonPlugins } = require("@codeceptjs/configure");
const cwd = process.cwd();
const fs = require("fs");
const path = require("path");
setHeadlessWhen(process.env.HEADLESS);
setCommonPlugins();
function getDwJson() {
if (fs.existsSync(path.join(cwd, "dw.json"))) {
return require(path.join(cwd, "dw.json"));
}
return {};
}
const DEFAULT_HOST = getDwJson().hostname
? "https://" + getDwJson().hostname
: "";
const HOST = DEFAULT_HOST || "https://" + process.env.SFCC_HOST;
const metadata = require("./test/acceptance/metadata.json");
exports.config = {
gherkin: {
features: "./test/acceptance/features/**/*.feature",
steps: "./test/acceptance/steps/**/*.js",
},
cleanup: true,
coloredLogs: true,
output: "./output",
helpers: {
Puppeteer: {
url: HOST,
show: false,
windowSize: "1200x900",
waitForTimeout: 100000,
chrome: {
args: ["--no-sandbox"],
},
},
},
include: metadata.include,
bootstrap: null,
mocha: {},
plugins: {
screenshotOnFail: {
enabled: true,
},
retryFailedStep: {
enabled: true,
},
},
name: "sample-cicd",
};
6. その他
-
/output
にE2Eの結果が出力されますが デフォルトのgitignoreに入っていないので、入れておいてもいいかと思います
Salesforce B2C Commerce カートリッジソースコードのテストについて で紹介した通りローカルでもテスト可能なので、stagingにマージする前に一度ローカルでテストを必ず行い、テストコードを修正してください
ローカルでの実行
CircleCI はローカルでも実行できます。 詳細はCircleCI のローカル CLI の使用をご覧ください。
今回の実装を実行する場合は、下記のコマンドを実行します。
# .circleci/config.yml が有効かどうかの確認 (必ずしも毎回する必要はない)
circleci config validate
# ジョブの実行
circleci local execute --job install_test_deploy -e SFCC_OAUTH_CLIENT_ID="{APIクライアントID}" -e SFCC_OAUTH_CLIENT_SECRET="{APIクライアントシークレット}" -e SFCC_HOST="{デプロイ先のホスト名}"
- APIクライアントIDとAPIクライアントシークレットには Salesforce B2C Commerce sfcc-ciについての2. Account Managerで設定で作成したID・パスワードを利用します。
- デプロイ先のホスト名にはデプロイ先のホスト名を入力します。例:XXXXXX.sandbox.us00.dx.commercecloud.salesforce.com
※ 私のMacではうまくCircle CIが動かなかったのでDockere Desktopのバージョンを4.2.0にダウングレードしました。
Mac OS上でのcircle ciの問題:
https://github.com/CircleCI-Public/circleci-cli/issues/672
https://stackoverflow.com/questions/62217678/can-i-roll-back-to-a-previous-version-of-docker-desktop
Circle CI上での実行
Circle CIとGithubレポジトリを繋げる
コピーしたSFRAソースコードをアップロードしたGithubレポジトリをCircleCIと連携し、プロジェクトを作成します。
方法については割愛しますが、CircleCIのブログなど参照してみてください。
同時にstagingブランチも作っておいてください。
Circle CIへの環境変数の設定
作成したCircle CIプロジェクトの設定から環境変数を下記のように設定します。それぞれ、ローカルで実行時に引数として入力した値です。_STAGING
という値がついていますが、この変数の扱いについては cicd-environment.sh
のソースコードをご覧ください。
- SFCC_HOST_STAGING
- SFCC_OAUTH_CLIENT_ID_STAGING
- SFCC_OAUTH_CLIENT_SECRET_STAGING
実行結果
feature/test
というブランチを作成し、staging
にマージしました。
B2C Commerceの環境にもデプロイされアクティブ化されていることがわかります。
参考
B2C Commerceのコードのレプリケーション:
https://trailhead.salesforce.com/ja/content/learn/modules/b2c-admin-replication/b2c-admin-explore-replication
CircleCIの使い方:
https://qiita.com/gold-kou/items/4c7e62434af455e977c2
https://circleci.com/docs/ja/2.0/hello-world/
CircleCIの環境変数:
https://circleci.com/docs/ja/2.0/env-vars/
CI/CD Implementation Guide:
https://resources.docs.salesforce.com/rel1/doc/en-us/static/pdf/CI_CD_Implementation_Guide.pdf
Gruntを利用したJS APIでのサンプル:
https://github.com/SalesforceCommerceCloud/build-suite
b2c-toolsというCI/CDツールもあるので、今後取り上げたいと思います。
https://github.com/SalesforceCommerceCloud/b2c-tools