CircleCIでマトリックスビルド
CircleCIでマトリックスビルド
現状CircleCIではマトリックスビルドはサポートされていませんが、実現するための方法がいくつかあります。
matrix
キーがサポートされました 👏
Configuring CircleCI - CircleCI
node v8、v10、v12、それぞれの実行環境でライブラリのインストール、テスト、ビルドを行い、すべてに成功した場合のみ、ビルドした成果物をNode v10でデプロイしたいとします。また、テストとビルドは互いに依存しないが、インストールに依存するとします。
それぞれの実行環境ごとに、ジョブを定義する
次のように、実行環境(Nodeバージョン)ごとにジョブを定義することができます。
version: 2.1 jobs: install_deps: docker: - image: circleci/node:10 steps: - checkout - restore_cache: keys: - v1-npm-{{ .Branch }}-{{ checksum "package-lock.json" }} - v1-npm-{{ .Branch }}- - v1-npm- - run: npm install - save_cache: paths: - node_modules key: v1-npm-{{ .Branch }}-{{ checksum "package-lock.json" }} - persist_to_workspace: root: ./ paths: - node_modules "node-8": docker: - image: circleci/node:8 steps: - checkout - attach_workspace: at: ./ - run: npm test - run: npm run build "node-10": docker: - image: circleci/node:10 steps: - checkout - attach_workspace: at: ./ - run: npm test - run: npm run build - persist_to_workspace: # Node v10でビルドした成果物を永続化 root: ./ paths: - lib/ - node_modules "node-12": docker: - image: circleci/node:12 steps: - checkout - attach_workspace: at: ./ - run: npm test - run: npm run build deploy: docker: - image: circleci/node:10 steps: - checkout - attach_workspace: at: ./ - run: npm run deploy workflows: version: 2 matrix: jobs: - install_deps - "node-8": requires: - install_deps - "node-10": requires: - install_deps - "node-12": requires: - install_deps - deploy: requires: - "node-8" - "node-10" - "node-12"
test
とbuild
が直列に定義されていることが気になるところですが、テストとビルドが依存していないからと言って別ジョブに分けて定義したいという欲がでてくると、node-8-test
、node-8-build
、node-10-test
…をそれぞれ定義しなければならなくなるため、ちょっと厳しくなります。
version: 2.1 jobs: (略) "node-8-test": docker: - image: circleci/node:8 steps: - checkout - attach_workspace: at: ./ - run: npm test "node-8-build": docker: - image: circleci/node:8 steps: - checkout - attach_workspace: at: ./ - run: npm run build "node-10-test": docker: - image: circleci/node:10 steps: - checkout - attach_workspace: at: ./ - run: npm test "node-10-build": docker: - image: circleci/node:10 steps: - checkout - attach_workspace: at: ./ - run: npm run build - persist_to_workspace: root: ./ paths: - lib - node_modules "node-12-test": docker: - image: circleci/node:12 steps: - checkout - attach_workspace: at: ./ - run: npm test "node-12-build": docker: - image: circleci/node:12 steps: - checkout - attach_workspace: at: ./ - run: npm run build deploy: docker: - image: circleci/node:10 steps: - checkout - attach_workspace: at: ./ - run: npm run deploy workflows: version: 2 matrix: jobs: - install_deps - "node-8-test": requires: - install_deps - "node-8-build": requires: - install_deps - "node-10-test": requires: - install_deps - "node-10-build": requires: - install_deps - "node-12-test": requires: - install_deps - "node-12-build": requires: - install_deps - deploy: requires: - "node-8-test" - "node-8-build" - "node-10-test" - "node-10-build" - "node-12-test" - "node-12-build"
この場合、パラメータを使ってジョブコンフィグをまとめることができます。
ジョブパラメータで管理する
パラメータを使えば、テスト・ビルドのジョブは使い回すことができるため、設定ファイルがすっきりします。ただし、これでも手動で値を入れる必要があるため、2軸のマトリックス(言語バージョン×OSなど)はうまく管理できる気がしません。
version: 2.1 executor-template: &executor-template parameters: tag: default: "10" type: string persist_built_files: type: boolean default: false docker: - image: circleci/node:<< parameters.tag >> jobs: install_deps: <<: *executor-template steps: - checkout - restore_cache: keys: - v1-npm-{{ .Branch }}-{{ checksum "package-lock.json" }} - v1-npm-{{ .Branch }}- - v1-npm- - run: npm install - save_cache: paths: - node_modules key: v1-npm-{{ .Branch }}-{{ checksum "package-lock.json" }} - persist_to_workspace: root: ./ paths: - node_modules test: <<: *executor-template steps: - checkout - attach_workspace: at: ./ - run: npm run test build: <<: *executor-template steps: - checkout - attach_workspace: at: ./ - run: npm run build - when: condition: << parameters.persist_built_files >> steps: - persist_to_workspace: root: ./ paths: - node_modules - lib deploy: <<: *executor-template steps: - checkout - attach_workspace: at: ./ - run: npm run deploy workflows: version: 2 matrix: jobs: - install_deps - test: name: node-8-test tag: "8" requires: - install_deps - test: name: node-10-test tag: "10" requires: - install_deps - test: name: node-12-test tag: "12" requires: - install_deps - build: name: node-8-build tag: "8" requires: - install_deps - build: name: node-10-build tag: "10" persist_built_files: true requires: - install_deps - build: name: node-12-build tag: "12" requires: - install_deps - deploy: tag: "10" requires: - node-8-test - node-8-build - node-10-test - node-10-build - node-12-test - node-12-build
パイプラインパラメータで管理する
少しトリッキーですが、パイプラインパラメータを使えば、ワークフロー全体にパラメータを渡すことができるため、より少ない記述量で実現できます。この設定ファイルを使用するには、APIトークン(CIRCLE_TOKEN
)が必要です。
(パラメータを使った動的なジョブ名は前からできたっけ?)
parameters: tag: default: '10' type: string run-main-workflow: default: false type: boolean run-deploy-job: default: false type: boolean executors: node: docker: - image: circleci/node:<< pipeline.parameters.tag >> version: 2.1 jobs: install_deps: executor: node steps: - checkout - restore_cache: keys: - v1-npm-{{ .Branch }}-{{ checksum "package-lock.json" }} - v1-npm-{{ .Branch }}- - v1-npm- - run: npm install - save_cache: paths: - node_modules key: v1-npm-{{ .Branch }}-{{ checksum "package-lock.json" }} - persist_to_workspace: root: ./ paths: - node_modules test: executor: node steps: - checkout - attach_workspace: at: ./ - run: npm run test build: executor: node steps: - checkout - attach_workspace: at: ./ - run: npm run build - when: condition: << pipeline.parameters.run-deploy-job >> steps: - persist_to_workspace: root: ./ paths: - lib - node_modules deploy: docker: - image: circleci/node:<< pipeline.parameters.tag >> steps: - when: condition: << pipeline.parameters.run-deploy-job >> steps: - checkout - attach_workspace: at: ./ - run: npm run deploy - unless: condition: << pipeline.parameters.run-deploy-job >> steps: - run: echo "No deployment on Node v<< pipeline.parameters.tag >>" trigger-main-workflows: machine: image: ubuntu-1604:201903-01 parameters: deploy-node-version: default: '10' type: string steps: - run: name: Trigger main worflow command: | VCS_TYPE=$(echo ${CIRCLE_BUILD_URL} | cut -d '/' -f 4) for NODE_VERSION in 8 10 12 do PIIPELINE_PARAM_MAP="{\"run-main-workflow\": true, \"tag\":\"$NODE_VERSION\"}" if [ "$NODE_VERSION" = << parameters.deploy-node-version >> ] then PIIPELINE_PARAM_MAP="{\"run-main-workflow\": true, \"tag\":\"$NODE_VERSION\", \"run-deploy-job\": true}" fi curl -u ${CIRCLE_TOKEN}: -X POST --header "Content-Type: application/json" -d "{ \"branch\": \"${CIRCLE_BRANCH}\", \"parameters\": ${PIIPELINE_PARAM_MAP} }" "https://circleci.com/api/v2/project/${VCS_TYPE}/${CIRCLE_PROJECT_USERNAME}/${CIRCLE_PROJECT_REPONAME}/pipeline" done workflows: version: 2 matrix: unless: << pipeline.parameters.run-main-workflow >> jobs: - trigger-main-workflows: deploy-node-version: "10" main: when: << pipeline.parameters.run-main-workflow >> jobs: - build: name: build-<< pipeline.parameters.tag >> - test: name: test-<< pipeline.parameters.tag >> - deploy: requires: - build-<< pipeline.parameters.tag >> - test-<< pipeline.parameters.tag >>
trigger-jobs
ワークフローが、Node v8, v10, v12の実行環境において、それぞれmain
ワークフローをトリガーします。
Nodeバージョン×DBといったマトリックスも可能です。
(略) trigger-main-workflows: machine: image: ubuntu-1604:201903-01 parameters: steps: - run: name: Trigger main worflow command: | VCS_TYPE=$(echo ${CIRCLE_BUILD_URL} | cut -d '/' -f 4) for NODE_VERSION in 8 10 12 do for DB in mongo mysql do PIIPELINE_PARAM_MAP="{\"run-main-workflow\": true, \"tag\":\"$NODE_VERSION\", \"db\":\"$DB\"}" if [ "$NODE_VERSION" = "10" ] && [ "$DB" = "mongo" ] then PIIPELINE_PARAM_MAP="{\"run-main-workflow\": true, \"tag\":\"$NODE_VERSION\", \"db\":\"mongo\", \"run-deploy-job\": true}" fi curl -u ${CIRCLE_TOKEN}: -X POST --header "Content-Type: application/json" -d "{ \"branch\": \"${CIRCLE_BRANCH}\", \"parameters\": ${PIIPELINE_PARAM_MAP} }" "https://circleci.com/api/v2/project/${VCS_TYPE}/${CIRCLE_PROJECT_USERNAME}/${CIRCLE_PROJECT_REPONAME}/pipeline" done done
ただし、APIを叩く必要があるため、これでもまだ他のCIでできるようなマトリックスビルドと比べれば、やぼったさは否めません。また、デプロイジョブをすべてのワークフローが成功した場合のみ実行するしくみ(ファンイン)にすることが非常に難しく、APIを使ってビルド結果をポーリングするか、承認ジョブを使って目視で確認してから手動で起動にするしかなさそうです。
RFC: Matrix Jobs syntaxを見ると、マトリックスビルドの開発が今まさに行われているようなので、正式サポートされるまではファンアウトだけなら上記のパイプラインパラメータ、ファンインが必要ならジョブパラメータが良さそうかな。
CircleCIからnpm packageを公開するためのOrb
作りました。よろしくお願いします。
使い方
NPMトークンを取得し、環境変数NPM_TOKEN
としてセット。package.json
の情報に従ってパッケージを公開します。
モジュールのダウンロードやビルドはpre-publish-steps
パラメータを使用します。
orbs: npm-publisher: uraway/npm-publisher@x.y.z version: 2.1 workflows: build_publish: jobs: - npm-publisher/publish-from-package-version: publish-token-variable: NPM_TOKEN push-git-tag: true fingerprints: <fingerprints> pre-publish-steps: - restore_cache: keys: - v1-node-cache-{{ .Branch }}-{{ checksum "package-lock.json" }} - v1-node-cache-{{ .Branch }} - v1-node-cache- - run: npm install - run: npm build post-publish-steps: - save_cache: key: v1-node-cache-{{ .Branch }}-{{ checksum "package-lock.json" }} paths: - node_modules filters: branches: only: master
また、push-git-tag
オプションを有効にしてフィンガープリントをセットすればタグをコミットします。
配列の積和演算のアルゴリズム
配列の積和演算のアルゴリズム
「特別な配列」が与えられるので積和演算を行って値を返す。「特別な配列」は数値あるいは「特別な配列」をもつ。「特別な配列」内の数値をすべて加算するが、ネストレベルに応じてその和を乗算する。 例えば、
[x, y] → x + y [x, [y, z]] → x + 2(y + z) [1, [2, [3, 4]]] → 1 + 2 * (2 + 3 * (3 + 4)) → 47
アルゴリズム実装(1) $O(n)$
再帰関数を別途作るのが楽。見たまんまで、配列ならdepth
に1を加えて再帰関数を実行し、数値なら加算する。
function productSum(array) { return productSumHelper(array, 1) } function productSumHelper(array, depth) { let result = 0 for (const ele of array) { if (Array.isArray(ele)) { result += productSumHelper(ele, depth + 1) } else { result += ele } } return result * depth }
要素数n
回実行されるので、計算量は$O(n)$
二分探索木(binary search tree)から近似値を求める
二分探索木(binary search tree)から近似値を求める
二分探索木のデータとtarget
が与えられる。二分探索木のデータの中からtarget
の近似値を求める。
例えば次のような二分探索木のデータとtarget
5が与えられるとき、答えは6である。
8 /\ 3 10 /\ \ 1 6 14 /\ / 4 7 13 ref: https://ja.wikipedia.org/wiki/%E4%BA%8C%E5%88%86%E6%8E%A2%E7%B4%A2%E6%9C%A8
二分探索木の定義
二分探索木のデータ構造は「左の子孫の値 ≤ 親 ≤ 右の子孫の値」という制約を持つ。与えられた値の近似値を求めるには、一時的にそれまで見つかった近似値を保持しておいて、次の子ノード、孫ノード、…と子ノードが存在しなくなるまで探索を続ける必要があるため、計算量はどのアルゴリズムでも最低O(n)
となるはず。
子ノードが存在しないときに保持していた近似値が解となる。
与えられる引数
引数はtree
とtarget
が与えられる。
tree
オブジェクトは数値value
、tree
オブジェクトleft
、tree
オブジェクトright
のプロパティを持つ再帰的なオブジェクト。target
は数値。
アルゴリズム実装(1) O(n)
ノードが存在しないとき、tree
オブジェクトはnull
をとる。近似値を保持しておいて、tree === null
となるまでループを続ける。二分探索木の定義から、left
プロパティのvalue
は必ず現在のノードのvalue
より小さく、right
プロパティのvalue
は必ず現在のノードのvalue
より大きい。
function findClosestValueInBst(tree, target) { return traverseNode(tree, target, null); } function traverseNode(node, target, closest) { while (node !== null) { if (closest === null || Math.abs(target - closest) > Math.abs(target - node.value)) { closest = node.value } if (target < node.value) { node = node.left } else if (target > node.value) { node = node.right } else { break } } return closest }
アルゴリズム実装(2) O(n)
メモリ的には優しくないかもしれないが、再帰関数のほうが理解しやすいかも?
function findClosestValueInBst(tree, target) { return traverseNode(tree, target, null); } function traverseNode(node, target, closest) { if (node === null) { return closest } if (closest === null || Math.abs(target - closest) > Math.abs(target - node.value)) { closest = node.value } if (target < node.value) { return traverseNode(node.left, target, closest) } else if (target > node.value) { return traverseNode(node.right, target, closest) } return closest }
VS Code Extensionの公開を簡単にするCircleCI Orb作りました
VS Code Extensionの公開を簡単にするCircleCI Orb作りました
作りました: https://circleci.com/orbs/registry/orb/uraway/vsce
リポジトリ: https://github.com/uraway/vsce
使い方
環境変数としてVSCODE_MARKETPLACE_TOKENにPersonal Access Tokenをセットして、package.json
のバージョンフィールドを更新してコミットすればあとは自動的に公開されます。
push-git-tag
オプションをtrue
にしてフィンガープリントもつければ、タグリリースやってくれるのでかんぺき。キャッシュの処理はキャッシュキーがややこしくなりそうなので、post-install-steps
とpre-install-steps
でユーザー定義にしました。
orbs: vsce: uraway/vsce@0.0.3 version: 2.1 workflows: test-publish: jobs: - vsce/publish: filters: branches: only: master post-install-steps: - save_cache: key: v1-node-cache-{{ .Branch }}-{{ checksum "package-lock.json" }} paths: - node_modules pre-install-steps: - restore_cache: keys: - v1-node-cache-{{ .Branch }}-{{ checksum "package-lock.json"}} - v1-node-cache-{{ .Branch }} - v1-node-cache- push-git-tag: true ssh-fingerprints: <フィンガープリント>
Orb開発
orbs: orb-tools: circleci/orb-tools@9.0.0 your-orb: your-namespace/your-orb@<<pipeline.parameters.dev-orb-version>> parameters: dev-orb-version: default: 'dev:alpha' type: string run-integration-tests: default: false type: boolean jobs: integration-tests: executor: orb-tools/ubuntu steps: - checkout version: 2.1 workflows: integration-tests_prod-release: jobs: - integration-tests - orb-tools/dev-promote-prod-from-commit-subject: add-pr-comment: true bot-user: your-namespace fail-if-semver-not-indicated: true publish-version-tag: true ssh-fingerprints: <フィンガープリント> filters: branches: only: master orb-name: your-namespace/your-orb requires: - integration-tests when: << pipeline.parameters.run-integration-tests >> lint_pack-validate_publish-dev: jobs: - orb-tools/lint - orb-tools/pack: requires: - orb-tools/lint - orb-tools/publish-dev: orb-name: your-namespace/your-orb requires: - orb-tools/pack - orb-tools/trigger-integration-tests-workflow: name: trigger-integration-dev requires: - orb-tools/publish-dev unless: << pipeline.parameters.run-integration-tests >>
orb-toolsをフルに使う
https://circleci.com/orbs/registry/orb/circleci/orb-tools
orb-tools、これはもうOrb開発に必須。だいたいこれで良い。v9.0.0になってまた使いやすくなりました。orb-tools/lint
、orb-tools/pack
、orb-tools/publish-dev
はもちろん便利ですが、orb-tools/dev-promote-prod-from-commit-subject
ジョブがまた良い仕事してくれる。
- orb-tools/dev-promote-prod-from-commit-subject: add-pr-comment: true bot-user: your-namespace fail-if-semver-not-indicated: true publish-version-tag: true ssh-fingerprints: <フィンガープリント> filters: branches: only: master orb-name: your-namespace/your-orb
プルリクエストをマージするときに、下記図のようにコミットサブジェクトに[semver:patch|minor|major|skip]を入れるとsemverにそってプロモートしてくれます。
また、add-pr-comment: true
を渡して、環境変数PR_COMMENTER_GITHUB_TOKEN
としてbot-user
に対応するGitHubトークンをプロジェクトに追加すれば、そのプルリクエストに、Orb公開したよ、とコメントしてくれます。
さらに、publish-version-tag: true
を渡して、デプロイキーのフィンガープリントをセットすればタグコミットをプッシュしてくれます。さいこう
必要であればインテグレーションテスト
Orbによっては必要なこともあるインテグレーションテスト。vsce Orbでは、外部APIであるVS Code Marketplaceとの連携をテストしたいので、リポジトリ内にテスト用のVS Code Extension (vsce-orb-integration-test
)を持ち、そのExtensionの公開・非公開をワークフロー内で繰り返しています。API制限とかあるのかな?
https://github.com/uraway/vsce
workflows: integration-tests_prod-release: jobs: - vsce/publish: name: publish-vsce publish-token-variable: VSCODE_MARKETPLACE_TOKEN push-git-tag: false package-path: vsce-orb-integration-test pre-install-steps: - restore_cache: keys: - v1-node-cache-{{ .Branch }}-{{ checksum "vsce-orb-integration-test/package-lock.json" }} - v1-node-cache-{{ .Branch }} - v1-node-cache- post-install-steps: - save_cache: key: v1-node-cache-{{ .Branch }}-{{ checksum "vsce-orb-integration-test/package-lock.json" }} paths: - node_modules - vsce/unpublish: name: unpublish-vsce publish-token-variable: VSCODE_MARKETPLACE_TOKEN package-path: vsce-orb-integration-test requires: - publish-vsce
2つの数の和
2つの数の和
2つ以上のユニークな数字の集合A
の中から、ターゲット(target
)の数字の和となる数字のペアを見つけ出す。数字のペアは最大1つある。答えは値が小さい順に並べるとする。
例えばA
が1, 10, 5, -1、12、target
が15であれば、答えは10, 5のペアである。A
を数字の配列の引数、target
を数字の引数としてこれを実装する。
アルゴリズム実装(1) O(n2)
最も簡単なアルゴリズムは二重ループして、すべての要素の和のパターンを出すこと。計算量はO(n^2)
。使用しているsort
は2つの数値の比較なので無視する。
function twoNumberSum(A, target) { for (let i = 0; i < A.length; i++) { for (let j = 0; j < A.length; j++) { if (i === j) continue; if (A[i] + A[j] === target){ return [A[i], A[j]].sort((a, b) => a - b) } } } return []; }
アルゴリズム実装(2) O(n)
計算し終えた要素を配列store
に保存していき、ワンループで済ます。計算量はO(n)
ループ中の数値をx
とすると、ペアになる可能性のある数値y
はtarget - x
と表せる。store
にy
が存在しなければ、x
をstore
に保存する。
こちらも使用しているsort
は2つの数値の比較なので無視する。
function twoNumberSum(A, target) { const store = [] for (const x of A) { const y = target - x if (store.includes(y)) { return [x, y].sort((a, b) => a - b) } else { store.push(x) } } return []; }
includes
の実装が気になる。ハッシュテーブルに保存のほうがいいかも?
アルゴリズム実装(3) O(n log n)
まず、A
を小さい順に並び替える。ソートに関しては、JavaScriptの実装はブラウザにもよるがここではクイックソート、計算量O(n log n)
とする。
[-1, 1, 5, 10, 12]
現在の最小の値のインデックスをmin
、最大の値のインデックスをmax
とおくと
sum = array[left] + array[right]
となる。sum
がtarget
より大きい場合、値を小さくしたいのでright
を小さくする。逆にsum
がtarget
より小さい場合、値を大きくしたいので、left
を小さくする。
例えば初期状態でleft=0
、right=4
であるためsum=11
left right | | [-1, 1, 5, 10, 12]
target
はそれより大きいのでleft
を右に動かしてleft=1
、right=4
ならばsum=13
left right | | [-1, 1, 5, 10, 12]
target
はそれより大きいのでleft
を右に動かしてleft=2
、right=4
ならばsum=17
left right | | [-1, 1, 5, 10, 12]
target
はそれより小さいのでright
を左に動かしてleft=2
、right=3
ならばsum=15
left right | | [-1, 1, 5, 10, 12]
クイックソートと一回のループの計算量は
O(n log n) + O(n) = O (n log n)
であることからO(n log n)
答えのペアはユニークな数値となることに注意してこれを実装すると
function twoNumberSum(A, target) { A.sort((a, b) => a - b) let left = 0 let right = A.length - 1 while (left < right) { const sum = A[left] + A[right] if (sum === target) { return [A[left], A[right]] } else if (sum < target) { left++ } else if (sum > target) { right-- } } return []; }
バイナリサーチ
バイナリサーチ
問: ソートされた数値の配列と数値target
を受け取り、配列中のtarget
のインデックスを求める。その際、バイナリサーチを用いること。
アルゴリズム実装 $O(\log n)$
配列の要素数をn
としたとき、left=0
、right=n
の変数を作る。また、left
とright
の中央の位置をmid
とする。
array[mid] === target
ならtarget
が見つかったとして探索終了。left >= right
なら見つからなかったとして探索終了。array[mid] > target
ならtarget
は配列の左半分にあるので、right=mid-1
として0へarray[mid] < target
ならtarget
は配列の右半分にあるので、left=mid+1
として0へ
function binarySearch(array, target) { let left = 0 let right = array.length - 1 while (left <= right) { const mid = Math.floor((left + right) / 2) if (array[mid] > target) { right = mid - 1 } else if (array[mid] < target) { left = mid + 1 } else { return mid } } return -1 }
バイナリサーチは一回実行するごとに対象の配列の要素数が1/2になる。最悪の場合要素数が1になるまで実行されるので、計算量はその繰り返し回数をk
とすると
底数を無視して、$O(\log n)$となる