GitHub Actions のアクションとして同一リポジトリーで定義した Docker イメージを使う

この記事では GitHub Actions のアクションとして同一リポジトリーで定義した Docker イメージを使う方法を説明します。

GitHub Actions のワークフローで Docker イメージをビルドする方法などは扱いません。

ここでアクションとワークフローの単語を次のように使い分けます。

  • アクション
    • …/action.yaml で定義する
    • ワークフローから参照される
  • ワークフロー
    • /.github/workflows/….yaml で定義する
    • アクションを参照する

まずリポジトリーのディレクトリー構成を示します。

  • /
    • .docker
      • Dockerfile
      • entrypoint.sh
    • .github
      • actions
      • workflows

次にそれぞれのファイルの役割を説明します。

/.docker/Dockerfile

イメージを定義する Dockerfile です。/.dockerディレクトリーは任意です。どこでも構いません。

Dockerfile の中身は次のようにしました。

FROM pandoc/ubuntu:2.10.1

COPY entrypoint.sh /

RUN chmod +x /entrypoint.sh

ENTRYPOINT ["/entrypoint.sh"]

/.docker/entrypoint.sh

コンテナー内で実行するスクリプトです。

#! /bin/sh

set -e

echo "Hello, $1!"

/.github/actions/hello/action.yaml

アクションを定義するファイルです。/.github/actions/helloディレクトリーは任意ですが、action.yaml の直上までがアクションの識別子となります。この場合は /.github/actions/hello が識別子です。

name: hello

description: say hello

inputs:
  whom:
    description: who to say hello
    required: true
    default: world

runs:
  using: docker
  image: ../../../.docker/Dockerfile
  args:
    - ${{ inputs.whom }}

usingdocker を指定することでアクションとして Docker イメージを使用できます。inputs でアクションの引数を定義し、runs.argsdocker run 時の引数として渡しています。

/.github/workflows/build.yaml

ワークフローを定義するファイルです。/.github/workflows/….yaml の形式のパスになります。

on : push

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: ./.github/actions/hello
        with:
          whom: Kazuki

jobs.….steps.uses に先ほど定義したアクションの識別子を指定します。

これで、GitHub Actions のワークフローで Docker イメージをビルドしてコンテナーを起動し実行することができます。

ちなみに Docker Hub にアップロードされたイメージを利用する場合は下記のように書けます。

jobs:
  build:steps:- uses: docker://alpine:3.8
      …

参照

docs.github.com

アクションとワークフローを別のリポジトリーにする場合は上記で解説されています。

docs.github.com

同一リポジトリー内に定義したアクションを参照する方法は上記で解説されています。

docs.github.com

GitHub Actions のアクションとする場合の Dockerfile の書き方は上記で解説されています。

docs.github.com

action.yaml の文法と意味については上記で解説されています。

docs.github.com

/.github/workflows/….yaml の文法と意味については上記で解説されています。

shake + lucid + hint で静的ウェブサイト生成

The English version is at Dev.


同人活動用のウェブサイトがあって今までは Jekyll で生成していました。これを Shake + Lucid + Hint で作成した生成器に置き換えました。

doujin.kakkun61.com

shakebuild.com

hackage.haskell.org

hackage.haskell.org

ソースコードはこちらです。

github.com

経緯

GitHub Pages をホストに選択したので最初は自然に Jekyll を選びました。レールに乗っているうちはいいのですが外れたことをしようとすると難しくなってきました。

どうレールを外れようとしたのかを説明するためにウェブサイトの説明をします。このウェブサイトは自サークルで発行した同人誌の紹介をするもので、同人誌ごとのページとそれを一覧するページから成ります。同人誌は即売会で頒布するためそれぞれの同人誌には即売会の情報が付随します。そうなると、ある即売会でどの同人誌が頒布されたかを表示したくなりました。つまり、即売会ごとのページとそれを一覧するページが欲しくなりました。これを実現するのは Jekyll では難しくありました*1

最初にサイトを作成したのが2018年1月ごろで、その後すぐに別のものに移ろうと Hakyll を触ったりしたのですが Hakyll の API の設計は好きになれず結局放置していました。

それでしばらく移行計画は頓挫していたのですが GHC のビルドシステムに Shake が使われているのを見てこれを使えるのじゃないかと思いました。そして調べたら Shake を利用した静的サイト生成器として RibSlick がありました。

API の好みから Slick を利用してみて、自分に必要のない部分を削いでいったらほとんど Slick がなくなったので Shake を直接使うようになりました。

構成

下記が概要図でこれからこれについて説明していきます。

f:id:kakkun61:20200925234250p:plain

まずコンテンツの変更をするたびに GHC でのコンパイルとリンクをするのは時間がかかるのでやりたくありません。そこで Haskell インタープリターを組み込むことにしました。そのライブラリーが Hint です。

流れとしては、Shake を使ってルールを書き、Hint でインタープリターを埋め込んで、実行ファイル gen を作ります。そして gen を実行してコンテンツの Haskell ソースや画像などを読み込み HTML などに変換して出力します。

Data.hs は gen の生成にも、gen が実行するインタープリターからも使うので両方から読み込みます。

Shake のルールには Make のように成果物を指定して依存解決する「後向き」と、ソースを指定する「前向き」とがあります。今回は前向きを使用しています。「前向き」の場合は別途 Filesystem Access Tracer(fsatrace)が必要です。

実装

ディレクトリー構成は次のようにしました。

  • app
    • gen.hs — 生成器本体
  • content
    • book
      • xxx.hs — 同人誌ごとのページ
    • image
    • lib
      • Layout.hs — コンテンツを囲むレイアウト
    • style
    • xxx.hs — その他のページ
  • lib
  • doujin-site.cabal

gen のさわりはこの辺りです。

lucid :: forall p r. (Show p, Typeable r) => FilePath -> FilePath -> p -> Shake.Action r
lucid source destination param = do
  libs <- Shake.getDirectoryFiles "content/lib" ["*.hs"]
  result <- liftIO $ Hint.runInterpreter $ do
    Hint.set [Hint.languageExtensions := [Hint.DuplicateRecordFields, Hint.OverloadedStrings]]
    Hint.loadModules $ ("content" </> source) : (("content/lib" </>) <$> libs)
    Hint.setTopLevelModules ["Main"]
    Hint.setImports ["Data.Functor.Identity", "Lucid", "Data.Text"]
    Hint.interpret ("render (" ++ show param ++ ")") (Hint.as :: Lucid.Html r)
  case result of
    Left e  -> do
      liftIO $ hPutStrLn stderr $ displayException e
      fail "interpret"
    Right html -> do
      Shake.writeFile' ("out" </> destination) $ show html
      pure $ runIdentity $ Lucid.evalHtmlT html

大して難しいことはしていませんが、1つ制約があって、それは show param の結果が妥当な Haskell コードになっていることです。自動導出していれば問題ないはずですが、自前で show を実装していると動作しないかもしれません。

コンテンツの Haskell ソースは render :: Typeable r => p -> Html r を露出していることにしています。book/xxx.hs は下記の形をしています。

{-# LANGUAGE OverloadedStrings #-}

import           Data
import qualified Layout as L

import Lucid

render path = do
  L.top (L.ogp ogp) $ L.book book (Just content)
  pure book
  where
    ogp = …
    book = …
    content =

返り値の Book はそれぞれのページの分が集められた後 index.hs に渡され一覧ページの作成に使用されます。

{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE NamedFieldPuns        #-}
{-# LANGUAGE OverloadedStrings     #-}

import           Data
import qualified Layout as L

import Data.Foldable as F
import Lucid

render (path, books) =
  L.top
    (L.ogp ogp)
    $ div_ [class_ "home"] $ do
        ul_ [class_ "book-list"] $ do
          F.for_ books $ \(Book { title, bookImage, events }, path) -> do
            li_ $ do
              h3_ $
                a_ [class_ "post-link", href_ path] $ toHtml title
              div_ [class_ "justify-bottom"] $ do
                ul_ [class_ "event-badges"] $ do
                  F.for_ events $ \Event { title } -> do
                    li_ [class_ "event-badge"] $ toHtml title
                    " "
                a_ [href_ path] $ img_ [src_ bookImage, alt_ "book image", class_ "home-book-front"]
  where
    ogp =

HTML の記述には Lucid を使用しました。内部 DSL なのでHaskell コードと統一した記法で埋め込めるので楽です。ここは好みで取り替えができます。

ここまで来れば即売会ページとその一覧ページを作るのも簡単そうです。

まとめ

  • Shake を依存関係記述に使う
  • Hint でインタープリターを埋め込む
  • Lucid で HTML を記述する
    • EDSL なので統一した記法で書ける

感想としては、型にはまったフレームワークでコンテンツを書くよりも、パーツとしてのライブラリーを組み合わせて作る方が柔軟性が高くて好みだなと再確認しました。

追記

2020.09.27

テンプレートリポジトリーにしました。

github.com

*1:少なくとも当時の Jekyll では。

「共有した URL を別のアプリで開く」2.0 をリリースしました

play.google.com

どういうアプリかというと EXTRA_TEXTURI に入ったインテントACTION_SEND でやってくると ACTION_VIEWインテントを投げ直すアプリです。

地味に50万超インストールされて、なぜかブラジルでよく使われているようです。

バージョン 2.0 は実に6年ぶりのアップデートでした。

今回の変更点は下記の通りです。

  • minSdkVersion: 9 → 17
    • Android 2.3 ~ 4.1 がサポート対象外に
  • targetSdkVersion: 21 → 29
  • Intent.createChooser() を使わず自前表示
    • 詳細は後述
  • ローンチャー(ホーム)にアプリアイコンを追加
    • 起動のしかたが分からない人がいて、低評価されるので
    • オプションで非表示にできる

「デフォルトのアプリ」や「アプリリンク」

Android のいつのバージョンからか「デフォルトのアプリ」という機能が増えています。設定アプリで〔アプリと通知〕→〔デフォルトのアプリ〕にあります。

f:id:kakkun61:20200923154958p:plain

これでブラウザアプリとして Chrome などを指定していると Intent.createChooser() でも PackageManager.queryIntentActivities() でもそれしか出てきません。

アプリリンクの場合も同じでアプリリンク用のアプリだけが選択肢に出てきます。

これを回避してインテントを受け取れるアプリ全てを取得する方法をご存じなら教えてほしいです。

Intent.createChooser() から PackageManager.queryIntentActivities() に変えれば取得できるかと思って変えたのですが同じでした。ただ選択肢が1つのときに Intent.createChooser() ならそのままアクティビティーが起動してしまうので、自前表示して選択肢を表示したかったので PackageManager.queryIntentActivities() に変えました。

今の実装の該当コードはこの辺りです。

        PackageManager pm = getPackageManager();
        Intent urlIntent = new Intent(Intent.ACTION_VIEW, uri);
        List<ResolveInfo> resolveInfos = pm.queryIntentActivities(urlIntent, 0);

英語に問題なければこちらに回答いただければ嬉しいです。

stackoverflow.com

鬱になりました

鬱になりました。

TwitterFacebook にはちらっと書いたのですが、で1月終わりから休職しています。ここらで一度ふりかえってみようかと思ったので書いてみます。

f:id:kakkun61:20200904160823j:plain

kokoro.mhlw.go.jp

何かおかしい

何かおかしいなと思ったのは正月の帰省から戻ってきた後でした。買い物に行きたいなと思いつつ全然部屋から出る気が起きず4日ぐらい1日スパゲッティ1食みたいな生活でした。元々出不精とはいえ出たいと思いつつ出れないのが続いてるなと思っていました。

その後しごとが始まりそれなりにしごとをしていましたが、朝の起きれなさ*1と起きても倦怠感や外に出たくなさがあり在宅勤務をする日が増えてきました。在宅勤務でしごとがはかどればよいのですが、今いちしごとに手が着かず週次の進捗報告で報告する進捗が減っていくのを感じていました。

さすがにこれはしごとをやっているとは言えないなという気持ちになり、出社する足で会社に行かずに精神科病院に行きました。

精神科病院が家の向かいであることはさいわいだったと思います。そうでなければグダグダと行く予定を先延ばしにしていたはずです*2

診察後上司にチャットでしばらく休職したいむねを報告しました。

休暇 1ヶ月め(2月)

最初の1ヶ月は積極的なことは何もせず休養をしていました。

意欲を完全に喪失していたので趣味のプログラミングもせずひたすらビデオを見ていました。『エウレカセブン』や『おジャ魔女どれみ』を全話見たりしました。

意欲を失った結果夜起きていてもしかたがないのでこのころ睡眠の周期がよくなりました。しかし日中もよく寝ていました。

この時期は意欲の喪失だけでなく思考能力もかなり落ちていました。プログラミングをしなかったのは意欲低下だけでなく思考能力の低下も理由にあります。

1ヶ月も休んだしそろそろ復職するかと人事や産業医に伝えたところ、まだ休職できるので十分休養しなさいということで休職を続けることにしました。後から思うと自分でもまだまだ休養すべきだったと思います。

休暇 2・3ヶ月め(3・4月)

鬱になってから気付いたのですが自分の場合心が凹むと部屋が乱雑になってくるようです。特に台所にそれが一番初めに表れます。心の凹みは注意しないと見逃してしまうので台所の乱雑さは自分の指標だと思うようになりました。

意欲が少しずつ回復してきたので暇を感じるようになりました。また少しは先のことを考えるようになりました。医者に相談したところ復職支援・再発防止のリハビリテーションがあるということでそれに参加するようにしました。それはリワークと呼ぶそうです。最終的には週に5日リワークに通うことが1つの目標です。

リワークでは、詳しいことは分かっていないのですが、認知再構成法や認知行動療法問題解決療法といった手法を学んだり、他の参加者とボードゲーム*3をすることを通じて、思考や対人コミュニケーションの訓練をしたりします。またヨガや創作活動によって悩みなどを一時的に忘れるようにしたりします。

趣味の面ではこのころ鉄道模型ジオラマを作っていました。手を動かす作業をするとイキイキした感じがしました。以前から気になっていた 3D プリンターをジオラマに使いたくなったので光造形式 3D プリンターを購入しました。いろいろとしていましたがプログラミングはまだひかえていました。

どれくらいプログラミングができていなかったのかプログラマーには分かりやすい図だと思うのですが GitHub のアクティビティーの図を下に示します。3月にちょろっと活動があるのは 3D モデルを GitHub にアップロードしたからです。

f:id:kakkun61:20200904140104p:plain

休暇 4・5ヶ月め(5・6月)

このころになるとちょこちょこプログラミングをし始めるようになりました。そしてその結果夜更かしが増えました。これはいけません。プログラミング、うまく問題が解決できるととても快楽を感じるのですが、なまじそれを知ったがためにもうちょっとでできそうというような気持ちでついつい夜更かししてしまいます。これはしごとでもそうで夕方から気分が乗ってきた結果、職場の電灯が消える時間までやってしまう傾向がありました。これは、直すべきだとこの休養中に認識したことの1つです。

夜更かしについてもそうですが、リワークの中でストレスを受けた状況をふりかえることがあります。そこで自分が課題であると認識したことの1つに対人コミュニケーションに苦手意識があるということです。しごとやその他において何か相談ごとがあるときに、うまく人に話しかけられない、話しかけるのに苦痛を感じるというのがあります。よくよくふりかえると今回の休職の原因にも関係しているように思えています。これは長い付き合いになると思いますがうまくやっていけるよう克服したいと思っています。いろいろと仮説や対策などを考えているのですが長くなるのでここでは割愛しましょう。

金銭面ですが、かなり貯金を切り崩してきました。傷病手当で給与の3分の2に相当するものがもらえるのですが該当期間の3・4ヶ月おくれということもあって現金がゴリゴリと減っていきました。生活するだけでかなりお金が必要なのだなと実感します。一部株式を手放しましたが、COVID-19 で一時下がっていた株価が持ち直していたので少し助かりました。

休暇 6・7ヶ月め(7・8月)

どうも気持ちには波があるようです。この時期は負の時期で睡眠周期が過去にないほど崩壊しました。昼夜逆転だけでなく長時間寝たり短時間を1日に何回も寝たりといったぐあいでした。そんな状況だったのでリワークにもほとんど通えず、進捗リセットという感じでそのことに凹みます。傷病手当申請と同時に会社に生活記録を提出する必要があるのですが、こうした時期は事務しごとがなかなか進まず申請が2ヶ月ほど遅れてしまいました。その影響でまた現金がギリギリです。

休職できる期限があるのでいつまでも休職するわけにはいかないのですが、復職するまでにまだ克服したいことが残っているのでしばらくはその訓練をすることになりそうです。

まだ書くことがあった気がしますが、公開しないと思い出せそうにないので公開してしまいます。

最後に

物的支援をよろしくお願いします!

ウィッシュリスト(食品・日用品)

ウィッシュリスト(趣味)

*1:元々睡眠障害をわずらっていましたが。

*2:普段から散髪など基本的に外出予定はズルズルと先延ばしになるので。

*3:お邪魔者』がおもしろかったです。

一番簡単な MonadFail インスタンス

The English version is at Dev.


導入

failMonad から剥がされて早や幾年、私は失敗する可能性のある計算は MonadFail を使って型を付けるのが好きです。

foo :: MonadFail m => m a

こうすると IO の文脈であればその中で、純粋な文脈であれば Maybe などで具体化して呼ぶことができます。

-- IO の文脈では
foo :: IO a

-- 純粋な文脈では
foo :: Maybe a

さて、純粋な文脈として Maybe を使うと失敗のメッセージを失ってしまうことが嬉しくありません。では、Either を使えばいいのではないでしょうか?実は EitherMonadFailインスタンスになっていません。提案はされていますが、失敗・成功以外に同列にパラメーターを扱うケースもあるのでそのときに MonadFail であることは適切でないからです*1

gitlab.haskell.org

そういうわけで一番簡単な MonadFail インスタンスとして次のような Result 型が欲しくなりました。

newtype Result a = Result (Either String a)

instance MonadFail Result where
  fail = Result . Left

実をいうとこれに相当するものはすでにあるのですが非推奨となっています。それは mtl パッケージの ErrorT です。

either-result パッケージ

そういうわけで Result に加えいくつかの関数をまとめてリリースしたのが either-result パッケージです。

hackage.haskell.org

実際には Resultモナドトランスフォーマー版の ResultT を使って実装され、ResultT は transformers パッケージの ExceptTnewtype です。

type Result a = ResultT Identity a

newtype ResultT m a = ResultT (ExceptT String m a)

ResultTExceptT と異なるのは MonadFail インスタンスで、fail を呼ぶと ResultTLeft でくるむのに対して ExceptT ではベースのモナドfail を呼びます。ですので、ResultT ではベースのモナドMonad しか要求しませんが、ExceptT では MonadFail であることを要求します。

instance Monad m => MonadFail (ResultT m) whereinstance MonadFail m => MonadFail (ExceptT e m) where

モナドトランスフォーマーにしたついでに mtl の MonadError インスタンスにもなっているので throwErrorcatchError することができます。

exceptions パッケージは?

MonadThrow という型クラスもなかったっけ?はい、あります。exceptions パッケージ*2MonadThrowManadCatch 型クラスがあります。こちらは投げる・捉えるものが Exception 型クラスであることを要求します。使い分けとしては、投げる・捉えるものを型で区別したい場合は MonadThrowMonadCatch にして、単にメッセージのみでよい場合は MonadFail にすればよいと思います。

class Monad m => MonadThrow m where
  throwM :: Exception e => e -> m a

class MonadThrow m => MonadCatch m where
  catch :: Exception e => m a -> (e -> m a) -> m a

class Monad m => MonadFail m where
  fail :: String -> m a

まとめ

  • 定義時、失敗する計算は MonadFail m => m a にしよう
  • 使用時、IO などの文脈では IO a などとして使おう
  • 使用時、純粋な文脈では Result a として使おう
  • GitHub リポジトリーの Star というボタンを押そう

*1:Rust ではデフォルトで Result 型で同列に扱いにくいため Either という名前がよかったという主張もあるみたいですね。

*2:使用する場合は safe-exceptions パッケージをおすすめします。

フィルムの在庫を数えた

SARS-CoV-2 の影響やらで最近フィルムの消費が少ないので期限切れフィルムを洗い出すことと冷蔵庫の中の整理のためにフィルムを全部出して数えてみた。

続きを読む

Twitter ツイートにウェブアプリを埋め込む

Twitter Card の Player だと iframe を使って埋め込めることを知ったので埋め込んでみた。(ツイートの埋め込みの中で埋め込んだものを再生できないので一旦ツイートを開く必要がある。)

f:id:kakkun61:20200728163346p:plain

Player Card を使ってるのはよく見るのは YouTube だろう。WebGL を埋め込んでる人もいた。

実装

実装としてはメタタグを埋め込むだけ(diff)。

    <meta name="twitter:card" content="player" />
    <meta name="twitter:site" content="@kakkun61" />
    <meta name="twitter:title" content="Photo Film Dev" />
    <meta name="twitter:description" content="This is a timer app for developing monochrome negative photograph films." />
    <meta name="twitter:image" content="https://photo-film-dev.kakkun61.com/preview.png" />
    <meta name="twitter:player" content="https://photo-film-dev.kakkun61.com/index.html" />
    <meta name="twitter:player:width" content="400" />
    <meta name="twitter:player:height" content="650" />

埋め込むコンテンツにはルールがあるので注意しよう。

developer.twitter.com