CircleCIでvscode-test
CircleCIでvscode-test
vscode-test にサンプルがなかったのでメモ
jobs: test: docker: - image: cimg/node:lts-browsers steps: - run: sudo apt update && sudo apt install libxss1 xvfb - run: | export DISPLAY=':99.0' /usr/bin/Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 & yarn test
Functional ComponentでforceUpdateをエミュレートする
Functional ComponentでforceUpdateをエミュレートする
参考
- https://www.telerik.com/blogs/what-is-render-react-how-do-you-force-it?utm_source=reactdigest&utm_medium=email&utm_campaign=274
- https://reactjs.org/docs/hooks-faq.html#is-there-something-like-forceupdate
- https://sung.codes/blog/2018/11/08/emulate-forceupdate-with-react-hooks/?preview_id=1931&preview_nonce=5e2a9a7f1b&preview=true&_thumbnail_id=1949#comment-4333911452
- https://www.reddit.com/r/reactjs/comments/6813z8/tell_me_any_real_use_case_where_forceupdate/dgwk210/?utm_source=reddit&utm_medium=web2x&context=3
Class ComponentのforceUpdate
実際にこのAPIを使うケースはほとんどないと思う。setState
を使ってもうまくコンポーネントが更新されないのでこのAPIを使いたい、といった場合はオブジェクトの同じ参照を渡していないかまずはチェックしてほしい。
Reactコアメンバーによれば、レガシーなコードとReactとの統合で使用するらしいが、幸運にもそのようなケースに出くわしたことはない。
export default class App extends React.Component { onClickHandler = () => { this.forceUpdate(); }; render() { return ( <button onClick={this.onClickHandler}>Click me</button> ); } }
Functional ComponentのforceUpdate
Functional Componentでも、useState
やuseReducer
を使えばエミュレートすることは可能。ただし非推奨
const useForceUpdate = (): [() => void, number] => { const [count, setCount] = React.useState(0); const increment = () => setCount((prev) => prev + 1); return [increment, count]; }; export default function App() { const [forceUpdate] = useForceUpdate(); const onClickHandler = () => { forceUpdate(); }; return ( <button onClick={onClickHandler}>Click me</button> ); }
Reactの描画プロセス
ではなぜ非推奨のAPIを紹介したのかというと、このforceUpdate
がReactの描画プロセスを理解するのに役立つと思ったから。
Reactの描画プロセスは次の流れに沿って行われる:
- Render:
render
メソッドを実行した結果を集め、オブジェクトツリーを形成する - Reconciliation: 新しいオブジェクトツリーと前回との差分を検出する
- Commit: 差分を実際のDOMに反映する
ここで重要なのは仮想DOMの差分だけが実際にDOMに反映されるということ。上記で紹介したサンプルコードでいくらforceUpdate
を実行しても、<button>
が書き換えられることはない。DOMインスペクタで検証してみてもDOMの更新を意味するflashが表示されていないことが分かる。
ただし、次のように関数の実行によって参照が変わるコンポーネントを使用すると、変更がなさそうに見えても差分は一旦破棄(アンマウント)され実際のDOMは更新される。
import * as React from "react"; import "./styles.css"; const useForceUpdate = (): [() => void, number] => { const [count, setCount] = React.useState(0); const increment = () => setCount((prev) => prev + 1); return [increment, count]; }; export default function App() { const [forceUpdate] = useForceUpdate(); const onClickHandler = () => { forceUpdate(); }; const Child = () => <div>child</div>; return ( <> <Child /> <button onClick={onClickHandler}>Click me</button> </> ); }
Rails Active Storage で画像ファイルをHTTPSで配信したい
HTTPSページにて、AWS S3に保存された画像ファイルをActive Storageで配信すると、Mixed content
エラーが発生します。
ref:
https://www.mixedcontentexamples.com/Test/NonSecureImage
https://www.mixedcontentexamples.com/
デバッガーツールのSecurity
タブからも確認できます。
これはRailsのurl_helper
メソッドがデフォルトではHTTPを使うため。
config.force_ssl = true
とした上で、config/initializers/force_ssl.rb
ファイルを作成し
Rails.application.routes.default_url_options[:protocol] = 'https' if Rails.application.config.force_ssl
とすれば良さそうです
ref:
https://medium.com/@stacietaylorcima/rails-active-storage-serve-your-images-over-https-14b916c67a51
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 }