5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Salesforce B2C Commerce SFRAでCI/CDやってみる

Last updated at Posted at 2022-06-08

※ これから記載する事項は、私が所属する会社とは一切関係のない事柄です。

この記事では前回紹介した Salesforce B2C Commerce sfcc-ciについてSalesforce B2C Commerce カートリッジソースコードのテストについて を応用してCircleCIを利用し、子ブランチがstagingにマージされてからデプロイされるまでの流れを作ってみました。

今回目指す仕様

  • stagingブランチへプッシュ(マージ)された時に実行される
  • コンテナ内に依存関係のインストール
  • ユニットテストの実行
  • 静的リソースのビルド
  • ソースコードのデプロイ
  • ソースコードのアクティブ化
  • インテグレーションテストの実行
  • E2Eテストの実行
  • /cartridges 内にカートリッジを追加していく
  • /sample_sitedata にサイトにインポートするソースコード追加する
  • インテグレーション又はE2Eテストでエラーだった場合はソースコードはサーバーに残りますが、以前アクティブだったソースコードバージョンに戻します
    スクリーンショット 2022-06-08 9.35.28.png

今回取り扱わない内容

  • 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

スクリーンショット 2022-06-08 9.01.03.png

cicd-install.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
cicd-environment.sh
#!/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
cicd-deploy.sh
#!/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
cicd-test.sh
#!/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 を下記のように書き換えます。
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 内に下記の内容を記載。

.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 に下記の内容を記載。

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="{デプロイ先のホスト名}"

※ 私の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

スクリーンショット 2022-06-08 10.04.34.png

実行結果

feature/test というブランチを作成し、staging にマージしました。
スクリーンショット 2022-06-08 10.11.14.png

すると、ワークフロージョブが実行され、無事完了しました。
スクリーンショット 2022-06-08 10.11.40.png

B2C Commerceの環境にもデプロイされアクティブ化されていることがわかります。

スクリーンショット 2022-06-08 11.31.56.png

参考

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

5
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?