特定のブランチ以外でGitHub Actionsを実行する方法をちょっと迷ったのでMEMO📝
結論からいうと下記です!
on: push: branches: - '**' # matches every branch - '!master' # excludes master
まずはすべてマッチするようにして、その後に!ブランチ名
で特定のブランチだけ除外できるんですね🙌
便利✨
特定のブランチ以外でGitHub Actionsを実行する方法をちょっと迷ったのでMEMO📝
結論からいうと下記です!
on: push: branches: - '**' # matches every branch - '!master' # excludes master
まずはすべてマッチするようにして、その後に!ブランチ名
で特定のブランチだけ除外できるんですね🙌
便利✨
RuboCop
は、Rubocop::Cop::Cop
を継承することで独自のルールを追加することできるようで、
レビューでよくコメントもらう内容等、RuboCop
で検知できるようになったら便利だなと思い、
ルールを作成してみたので、そのへんの手順等をMEMOしておきます✍
公式の開発用のDocはこちら📝
今回は下記のようなことを検証するルールを作ってみました🤖
SQLの実行回数削減のため、pluck
を使っていたらselect
を使うことをサジェストする
# bad Foo.where(id: Bar.pluck(:foo_id)) # good Foo.where(id: Bar.select(:foo_id))
where
内でLIKE
を使っていたらsanitized_sql_like
を使うことをサジェストする
# bad Foo.where('title LIKE ?', "%#{title}%") # good Foo.where('title LIKE ?', "%#{sanitized_sql_like(title)}%")
作ったものは下記のリポジトリにアップしています。
新しいルール作ってrubocop
実行時に静的解析を行うようにするには下記が必要なようです👀
RuboCop::Cop::Cop
を継承したClassを作るRuboCop::Cop::Cop
を継承したClassのテストを書く.rubocop.yml
でrequire
するRuboCop::Cop::Cop
を継承したClassを作る今回、私が実際に作ったClassは下記のような形です。
下記は、SQLの実行回数削減のため、pluck
を使っていたらselect
を使うことをサジェストするルールです。
require 'rubocop' module CustomCops # @example # # bad # Foo.where(id: Bar.pluck(:foo_id)) # # # good # Foo.where(id: Bar.select(:foo_id)) class UseSelectInWhere < RuboCop::Cop::Cop MSG = 'Use select instead of pluck in where to use subqueries to reduce SQL executions.' def_node_matcher :where_in_pluck_candidate?, <<~PATTERN (send _ :where (:hash (:pair (:sym _) (send _ :pluck (:sym _)) ...))) PATTERN def on_send(node) where_in_pluck_candidate?(node) do add_offense(node) end end def autocorrect(node) # TODO end end end
ちょっと解説すると
MSG
に設定された値が違反時にメッセージとして表示されるようです。def_node_matcher
でwhere
で{ sym: 任意のオブジェクト.pluck}
のような形で引数に渡しているか解析するmather where_in_pluck_candidate?
を定義しています。※このmatherは抜け漏れがあるかも。。。where_in_pluck_candidate?
に合致するようなものがあったらadd_offense
で警告を出しています。TODO
にしてしまっていますが、autocorrect
に処理を書くとautocorrect
の処理を定義出来ます。RuboCop::Cop::Cop
を継承したClassのテストを書く先程作ったClassのテストは下記のような形で行うようにしました。
RuboCop::RSpec::ExpectOffense
をincludeしてexpect_offense
に、コードとメッセージを渡してあげるような形でテストするようです👀
※expect_offense
内の記載内容をいい感じに取得する方法がわからなかったので、とりあえずテストを実行して失敗したときの値が、それっぽかったらコピペするような形にしたが、もっといい方法がありそう。。。
require 'rubocop' require 'rubocop/rspec/support' RSpec.configure do |config| config.include(RuboCop::RSpec::ExpectOffense) end describe CustomCops::UseSelectInWhere do subject(:cop) { described_class.new } it 'raised offense when use `pluck` in `where`.' do expect_offense(<<-RUBY) Foo.where(id: Bar.pluck(:foo_id)) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use select instead of pluck in where to use subqueries to reduce SQL executions. RUBY end end
.rubocop.yml
でrequire
する.rubocop.yml
で先程作ったclassのファイルをrequire
することでルールを追加します。
require: './cops/use_select_in_where'
rubocop
実行時に静的解析されるようになりました🙌
$ be rubocop sample/ sample/use_select_in_where.rb:4:23: C: CustomCops/UseSelectInWhere: Use select instead of pluck in where to use subqueries to reduce SQL executions. scope :recent, -> { Foo.where(id: Bar.pluck(:foo_id)) } ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
レビューでよくコメントが入る内容や、プロジェクト独自で拡張したクラスを使う必要がある場合等、rubocopの独自ルールとして定義しておくと、レビュー時の抜け漏れ等が防げて良さそうですね✨
Version 1.0に向けてCop周りの仕様の見直しが結構入っているようなので、これから作るときは下記を参考にしたほうが良さそう👀
rubocop/v1_upgrade_notes.adoc at master · rubocop-hq/rubocop · GitHub
最近Webpackで管理してるフロントエンドアプリをPWA対応してAndroid環境でホームに追加して起動できるようにしたので、そのへんの対応まわりをメモしておく✍
👇コードだけみたい人はこちら
Progressive web apps (PWA) は、新しいウェブ API と伝統的なプログレッシブな拡張戦略を使用して、クロスプラットフォームのウェブアプリケーションにネイティブアプリと同様の使い勝手をもたらすウェブアプリのことです。 https://developer.mozilla.org/ja/docs/Web/Progressive_web_apps
ウェブアプリケーションをネイティブアプリっぽく使うための拡張でPWAと呼ぶには技術的に下記の要素を持つことが必要なようです。
安全なコンテキスト (HTTPS)
(HTTPS / TLS を介して) コンテンツが安全に配信され、安全ではないコンテキストとの通信の可能性が限られているという合理的な確信がある Window、または Worker のことです。多くのウェブ API が安全なコンテキストでのみ利用可能です。 https://developer.mozilla.org/ja/docs/Web/Security/Secure_Contexts
サービスワーカー
ネットワークのリクエストに介在してネットワークの使用可否の状況に基づいて適切な対応を取ったり、サーバー上にあるアセットを更新したりします。プッシュ通知やバックグラウンド同期の API 群へのアクセスもできるようになります。 https://developer.mozilla.org/ja/docs/Web/API/Service_Worker_API
workbox-sw
、workbox-webpack-plugin
を使ってコードを生成しました。マニフェストファイル
PWA のマニフェストには、その名前、作者、アイコン、バージョン、説明、および (他のものの中で特に) 必要なすべてのリソースのリストが含まれています。 https://developer.mozilla.org/ja/docs/Web/Manifest
webpack-pwa-manifest
を使ってコードを生成しました詳しくはMDNのドキュメントをご確認ください
サービスワーカー用のスクリプトファイルの生成には下記のライブラリを使用しました。
workbox-sw
Googleが出しているサービスワーカー用のコード生成を支援してくれるライブラリ
workbox-webpack-plugin
workboxをwebpackから使用するためのプラグイン
まずはこれらをinstallしていきます📦
npm install --save-dev workbox-sw workbox-webpack-plugin
そしてwebpack.config.js
を下記のような感じで修正します。
※今回はdist
配下にservice-worker.js
という名前で出力するようにしています。
const workBoxWebpackPlugin = require("workbox-webpack-plugin"); const OUTPUT_PATH = `${__dirname}/dist`; module.exports = { // ... 省略 plugins: [ // PWA用のservice-worker.jsを生成 new workBoxWebpackPlugin.GenerateSW({ swDest: OUTPUT_PATH + "/service-worker.js" }),
この状態でwebpack
を実行すると下記のような形でservice-worker.js
と関連ファイルが生成されます✨
$ ls dist/
index-35ccbfc3e6ee670c355d.css service-worker.js
index-35ccbfc3e6ee670c355d.js service-worker.js.map
index.html workbox-64f1e998.js
workbox-64f1e998.js.map
そしてサービスワーカーを登録するためには生成されたjsを読み込むようにentryのjsで下記を実行するようにします。
if ("serviceWorker" in navigator) { window.addEventListener("load", () => { navigator.serviceWorker.register("./service-worker.js"); }); }
これでサービスワーカーの準備が出来ました🙆♂️
マニフェストファイルの生成にはwebpack-pwa-manifest
を使用しました。
まずはinstallします📦
npm install --save-dev webpack-pwa-manifest
そしてwebpack.config.js
を下記のような感じで修正します。
const workBoxWebpackPlugin = require("workbox-webpack-plugin"); const WebpackPwaManifest = require("webpack-pwa-manifest"); const OUTPUT_PATH = `${__dirname}/dist`; module.exports = { // ... 省略 plugins: [ // PWA用のservice-worker.jsを生成 new workBoxWebpackPlugin.GenerateSW({ swDest: OUTPUT_PATH + "/service-worker.js" }), // PWA用のmenifest.jsonを生成 new WebpackPwaManifest({ short_name: "short name", // ホーム画面のラベルに表示される名称 name: "app full name", // appの名前 display: "standalone", // standaloneにするとブラウザのUI要素が削除されてアプリっぽくなる start_url: "index.html", // 開始時に起動するページ }),
👇マニフェストファイルの項目の詳細はこちら
この状態でwebpack
を実行すると下記のような形でmanifest.json
が生成されます✨※最初からdigestが付与されていて便利
$ ls dist/
index-35ccbfc3e6ee670c355d.css service-worker.js
index-35ccbfc3e6ee670c355d.js service-worker.js.map
index.html workbox-64f1e998.js
manifest.ababbbaae2b52862d5cbfb8aefc3af66.json workbox-64f1e998.js.map
こんな感じのmanifest.json
が生成されます⚙
{ "name": "app full name", "short_name": "short name", "orientation": "portrait", "display": "standalone", "start_url": "index.html" } // "orientation": "portrait"は縦の向きを表しているようです。 // > the "portrait" enum represents the portrait orientation // > https://www.w3.org/TR/screen-orientation/#screenorientation-interface
これでmanifest.json
も生成され、PWAを最低限使う準備が出来ました🙌
iOSではPWAは未対応なのですが、Safariからホームへ追加した際の挙動を制御することができるようです。
htmlのhead
内に下記を追加するとホームに追加したときの挙動を制御できるようです👀
<head> <meta name="viewport" content="width=device-width,initial-scale=1.0,minimum-scale=1.0,user-scalable=no"> <!-- URLバー等を消してアプリのように見せる --> <meta name="apple-mobile-web-app-capable" content="yes"> <!-- ステータスバーのデザイン --> <meta name="apple-mobile-web-app-status-bar-style" content="black"> <!-- ホームに追加したときの名前 --> <meta name="apple-mobile-web-app-title" content="App Name"> </head>
PWA対応、ネイティブアプリに比べると色々と制約が多そう + まだ対応していないものが(特にios)多いので、 これから感がありますが既存のウェブアプリを意外とさくっとアプリっぽく出来ていいですね👍
第2、第4火曜日だけなんかしらの処理を実行したいとき等、指定日が第何火曜日を知りたいケースの場合に、RubyとかActiveSupportにこういう系のメソッドが無いかなと思ったんですが、 曜日判定は出来ても第何曜日の判定を行うようなメソッドは見当たらず、ちょっと迷って色々考えたのでメモしておきます✍
下記のような方針で考えてみました📅
Date#wday
を使って曜日を表す数字を取得して、hashで文字列を取得Date#cweek
を使って取得して差分を取得実際のコードはこちら
def nth_day_of_week(now: Time.current) wdays = { 0 => "日", 1 => "月", 2 => "火", 3 => "水", 4 => "木", 5 => "金", 6 => "土" } # NOTE: 月初と現在が同じ週だった場合に1としたいので、月の最初の週は0とするためcweekから1を引く beginning_of_month_cweek = now.beginning_of_month.to_date.cweek - 1 nth = now.to_date.cweek - beginning_of_month_cweek # NOTE: 年跨ぎの場合、nthが上記方針だとマイナスになるケースがあるので、その場合は前週の結果に+1する方針とする # 例) "2019/12/31".in_time_zone.to_date.cweek => 1, "2019/12/1".in_time_zone.to_date.cweek => 48 nth = now.ago(1.week).to_date.cweek - beginning_of_month_cweek if nth.negative? return { nth: nth, wday: wdays[now.wday] } end nth_day_of_week # => {:nth=>2, :wday=>"日"} nth_day_of_week(now: 1.week.ago) # => {:nth=>1, :wday=>"日"}
考慮漏れとかもあるかもですが、一旦こんな感じで取れそうでした・・・!(考えてみると意外と難しい・・・🤔💦)
Twitterで教えてもらった方法、月初を取得しないのでシンプル✨
何回目かはcweekよりも `https://t.co/sIZk1YThtR / 8 + 1` で計算した方が楽そうかなと思いました。ISO8601の週の話だけ面倒なので、そこだけif文要るかもしれないですが。
— 神速 (@sinsoku_listy) 2020年6月14日
def nth_day_of_week(now: Time.current) wdays = { 0 => "日", 1 => "月", 2 => "火", 3 => "水", 4 => "木", 5 => "金", 6 => "土" } nth = now.day / 8 + 1 return { nth: nth, wday: wdays[now.wday] } end
この辺の考慮は別途必要・・・!(割と大変)※でもこの辺の年末の考え方は、12月が12月として扱いたいとかありそうなので、要件によって変わるかも?
具体例を以下に示す。年初において以下の曜日に該当する場合、その日は新年第1週の日としてではなく、旧年最終週の日として扱う。
* 1月1日金曜日・1月2日土曜日・1月3日日曜日
* 1月1日土曜日・1月2日日曜日
* 1月1日日曜日
同様に、年末において以下の曜日に該当する場合、その日は旧年最終週の日としてではなく、新年第1週の日として扱う。
* 12月31日月曜日
* 12月30日月曜日・12月31日火曜日
* 12月29日月曜日・12月30日火曜日・12月31日水曜日
https://ja.wikipedia.org/wiki/ISO_8601
それにしても年またぎとかの考慮難しい・・・。
自分でwebpack
+ TypeScript
+ Vue.js
の環境を作ったので、勉強がてらYouTube Data APIを使ってTikTokぽく見れるViewerを作ってみました📺
動作はこんな感じです👇(デザインはいまいちですが・・・)
触ってみたい人はこちらから(YouTube Data APIのAPI KEYが必要です)
webpack
とTypeScript
を触ったのはほぼ初めてだったので、自分で1から環境を作って実装してみて色々ハマりどころも多かったのですが、こういうフレームワークに隠蔽されている部分の車輪の再発明をしとくと仕組みとかを理解出来て良いですね。
今回自分で作ったwebpack
+ TypeScript
+ Vue.js
の環境はこちらのリポジトリに公開してます。
普段書いてるのがRubyなので、型を書くの結構めんどくさいなと思ってたのですが、IDEの補完とかnil周りの考慮漏れとかに気付けるのは結構メリットとして大きいのかなと思いました。
特にAPIとかとのインターフェイス部分とかは書いとくと安心感あるなと思いました。
それでは👋
下記のような設定ファイルで動かしていたTravis CI上でheadless chrome使うようなgemのテストが急に落ち始めて、テスト通るところまで直したので色々メモして残しておきます✍
--- dist: trusty language: ruby cache: bundler rvm: @@ -8,13 +7,6 @@ rvm: - 2.5.8 addons: chrome: stable apt: packages: - chromium-chromedriver before_install: - export TZ=Asia/Tokyo - gem install bundler -v 2.1.4 before_script: - "export DISPLAY=:99.0" - "sh -e /etc/init.d/xvfb start" - sleep 3
修正したPRだけみたい場合はこちら
下記のような形でchromeのversionが古いということで急にテストが落ち始めた。
1) TravelToJavascript locks time in javascript and restore time outside block. Failure/Error: session.visit('/') Selenium::WebDriver::Error::SessionNotCreatedError: session not created exception: Chrome version must be >= 67.0.3396.0 (Driver info: chromedriver=2.41.578700 (2f1ed5f9343c13f73144538f15c00b370eda6706),platform=Linux 4.4.0-101-generic x86_64)
実際にci上でscriptを走らせてchromeのversionを確認してみたところ確かに古いversionのchromeが動いてしまっているようだった。
$ google-chrome --version Google Chrome 62.0.3202.94
原因はdist: trusty
を使用していることのようだった・・・!
Travis CIのコミュニティで同じような状況の人がいないかなと思って検索したら、同様の事象が発生している人がいたのですが、
回答を見ると、下記の通りXenial
を使用することが推奨されているようだった。
Google is serving a package that can’t be installed with Trusty. Use Xenial or later.
どうやらGoogleが提供しているパッケージがtrusty
ではinstall出来なくなってしまったみたいです。(失敗してデフォルトの古いchromeが使われてしまった?)
確かに、jobのログを見てみるとinstall時に失敗しているようだった💦
$ export CHROME_SOURCE_URL=https://dl.google.com/dl/linux/direct/google-chrome-stable_current_amd64.deb Installing Google Chrome stable 0.56s$ wget --no-verbose -O /tmp/$(basename $CHROME_SOURCE_URL) $CHROME_SOURCE_URL 2020-05-23 06:15:49 URL:https://dl.google.com/dl/linux/direct/google-chrome-stable_current_amd64.deb [68678632/68678632] -> "/tmp/google-chrome-stable_current_amd64.deb" [1] dpkg-deb: error: archive '/tmp/google-chrome-stable_current_amd64.deb' has premature member 'control.tar.xz' before 'control.tar.gz', giving up dpkg: error processing archive /tmp/google-chrome-stable_current_amd64.deb (--install): subprocess dpkg-deb --control returned error exit status 2 Errors were encountered while processing: /tmp/google-chrome-stable_current_amd64.deb
対策では上記のコミュニティの回答どおり.travis.yml
を下記のように修正してXenial
を使うようにしました。
--- language: ruby cache: bundler rvm: - 2.7.1 - 2.6.6 - 2.5.8 addons: chrome: stable before_install: - export TZ=Asia/Tokyo - gem install bundler -v 2.1.4
※dist未指定だとXenial
が使用される。https://docs.travis-ci.com/user/reference/overview/#linux
trustyってubuntuのバージョニングのことだったんですね😅(trustyはサポート終了していてSecurity Fix以外は行われないようでした。。。)
また別件で下記のエラーが発生して落ちるようになってしまったので、
Selenium::WebDriver::Error::UnknownError: unknown error: Chrome failed to start: crashed. (unknown error: DevToolsActivePort file doesn't exist) (The process started from chrome location /usr/bin/google-chrome is no longer running, so ChromeDriver is assuming that Chrome has crashed.)
Capybaraでのdriverの設定を修正しました。Selenium::WebDriver::Remote::Capabilities
を使ってoptionを指定していたのが良くなかった(?)🤔
before
Capybara.register_driver :headless_chrome do |app| driver = Capybara::Selenium::Driver.new( Capybara::Selenium::Driver.new( app, browser: :chrome, desired_capabilities: Selenium::WebDriver::Remote::Capabilities.chrome( login_prefs: { browser: 'ALL' }, chrome_options: { args: %w[headless disable-gpu window-size=1900,1200 lang=ja no-sandbox disable-dev-shm-usage], } ) ) driver end
after
Capybara.register_driver :headless_chrome do |app| options = Selenium::WebDriver::Chrome::Options.new options.add_argument('headless') options.add_argument('--disable-gpu') options.add_argument('--no-sandbox') options.add_argument('--disable-dev-shm-usage') Capybara::Selenium::Driver.new( app, browser: :chrome, options: options ) end
これらの対応を行ってCIが通るようになりました🍏
下記のような形でActiveRecordが発行するSQLからWHERE句の部分のSQLだけを取得したいケースがあり、色々やり方とか調べたのでMEMO✍
今回やりたかったのは、下記のようなscope
を定義していたときにto_sql
するとSQL全体を取得することが出来ますが、
class Book < ApplicationReacord scope :viewable, ->(now = Time.current) { where(published_at: now..) } end Book.viewable.to_sql #=> select * from books where published_at > '2020-05-23 15:24:34'
そうではなくWHERE
句のpublished_at > '2020-05-23 15:24:34'
部分だけ取得したいというものです。
下記のような感じで取れるみたいです👀(⚠nodocなので動作保証されてないですが・・・!)
Book.viewable.values[:where].ast.to_sql #=> published_at > '2020-05-23 15:24:34'
values[:where]
でActiveRecord::Relation::WhereClauseのオブジェクトを取得することができて、オブジェクトに対してastメソッドを呼び出すことによってArel::Nodes
系のオブジェクトを取得できるようです👀
そして取得したオブジェクトにto_sql
を呼び出すことによってそのNodeの部分だけのSQLを取得できるような形で動いてそうな気がしました・・・!
WHEREも含めて取得したいときは下記のようにするととれるようです👀(⚠こちらもnodocなので動作保証されてないです。)
Book.viewable.arel.where_sql #=> WHERE published_at > '2020-05-23 15:24:34'
ですが、Arel::Nodes::Node#to_sql
はコメントにも無くなる可能性が明記されてるので、素直にSQLをparseした方が良さそうな気もしました😓
# FIXME: this method should go away. I don't like people calling # to_sql on non-head nodes. This forces us to walk the AST until we # can find a node that has a "relation" member. # # Maybe we should just use `Table.engine`? :'( def to_sql(engine = Table.engine) collector = Arel::Collectors::SQLString.new collector = engine.connection.visitor.accept self, collector collector.value end
rails/node.rb at b1f6d8c8d8ad3e2e5b96e95b455c70f2c895ce14 · rails/rails · GitHub