CI回してMarkdownにバッジを置く、ただそれだけのこと(前編)

2021/12/24に公開

qiita-adc-d-20211124

この記事は Qiita Advent Calendar 2021 の D言語カレンダー 24日目 の記事です。

はじめに

世はまさに大Markdown時代。

Markdownで大量の技術記事が書かれ、社内文書も一部はMarkdownで書く世の中になってきました。

しかしそんな時代も数年が経過しています。
どうにも「古いMarkdown」と出会うことが増え、「サンプルコードが使えない」というケースも増えてきたように感じます。

これで動くんだっけ?
#include <studio.h>

void main(void) {
  printf("Hello, world!?");
}

そこで今回は、こういった誤字も含め、ちょっとしたツールを作って 「Markdownを検証する」 という文書の陳腐化軽減策をお手軽に実践、紹介していきます。

多少労力をかけても後世に残す技術記事を書きたいと思っている方、なんかコードを書くのに一種の飽きのようなものを感じている方、様々な人に面白いと思っていただければ幸いです。

課題

まず置かれている状況を簡単に整理すると、以下のようなことです。

  1. Markdownがいっぱいあるよ。最近は5年以上古いものも珍しくなくなってきたよ。
  2. たまにサンプルコードが動かないよ。大体どれも昔のバージョンを前提に書かれてるよ。
  3. でも、どれがダメなのか一見わからないよ。それに古くても良いMarkdownはあるかもね。

つまり、

  • 「Markdownが古い」と「中身が無効」は言ってしまえば無関係である
  • しかし、中身が有効かどうかなんて人が判断しないとわからない(今のところ)
  • 結果をすぐ分かるようにしたいが、何か良い方法があるか?

というのが課題です。

ここまで書いてしまえばタイトルとセットでピンと来る方はいるでしょう。

以下で具体的な対策の話をしていきます。

対策

Markdownに書かれた内容は、それが相当枯れたものでなければいつかは陳腐化するでしょう。

これはもう避けられそうにないので、1つ考えました。

名付けて 「コードブロック抜き出して繋げたら実行できるしCIも組めるでしょ作戦」 です。

いっそ日本語部分は忘れてコードブロックに絞ります。コード部分が動いてるなら日本語部分も多分大丈夫です。

これでやることは簡単5ステップ。

  1. Markdownからコードブロックを抜き出す
  2. 適当につなげたりしてソースコードにする
  3. 色々補って実行できるように整える(ここまでツールにする)
  4. CIでスケジュール実行する
  5. 実行結果をバッジとして取り出してMarkdownに貼り付けておく

そしてお気づきでしょう。この記事も冒頭に「バッジ」が付いていることに…

そう。この記事は、まさにこの作戦をやってみる実践記事というわけです。

別に何か適当なライブラリのバッジを貼っているわけではなくて、文字通り「このMarkdownのバッジ」というわけです。
Markdownをテストしたって誰も怒りません。たぶん。

冒頭のバッジが緑なら、以下に出てくるコードブロックのほとんどが動くことを保証された状態です。意気揚々と読み進められるはずですよね。逆にこれがいつダメになるのかと思うと、書いてる私はドキドキが止まりません!

では早速、続きをやっていきましょう。

コードブロックを動かす?

すぐ分かることですが、どんなコードブロックでも動かせるわけではありません。

プログラミング言語にはいわゆる「おまじない」があり、これがないと動かない記述というのが多々あります。

そこで、今回は運用のニュアンスが分かるように動かしやすいものをターゲットにします。

はい。対象とするのは、我らが 「D言語」 です。
なんかやる前から雑に扱っても動きそうな予感はすごくしてました。 これぞ抜群の信頼性。

なお他の言語でも大筋同じことはできると思うので、言語的な特徴は差し引いて読んでいただければと思います。

コードブロックを抜き出す

最初のステップです。コードブロックは言語が設定できるので、ここで抜き出す言語として dD をターゲットにします。

以下のいずれかです。

```d
```

あるいは

```D
```

これを適当なMarkdownのパーサーで取ってくれば良く、今回は CommonMark という仕様に乗っ取った commonmark-d というのを使いました。これで抜き出すところは一発です。さすがMarkdown様。

https://code.dlang.org/packages/commonmark-d

一番簡単なコードブロック

まさにサンプルですが、Hello, world!でこんな感じのコードです。

import std.stdio;
void main() {
    writeln("Hello, world!");
}

これは単体で動くやつを持ってきましたが、これだけ見ても正直あまり面白みはないですね。
ファイルに保存してコンパイルしてドン!で終わりです。

あ、一応これもちゃんと検証の対象です。

動かしたいコードブロック

Markdownのコードブロックは「説明用」です。
分割して説明文を入れる、前提部分を省略したり使いまわす、といったことを何も考えずに書くわけです。

つまり以下のように変数宣言をして、

auto message = "Hello, world!";

表示するところはあとで書く。

writeln(message); // Hello, world!

みたいな例が頻繁に出てきます。

これは「おまじない足りないので動かない」という例です。実際動かしようがないので検証対象外です。残念ですが。

こちらサンプルが分割されていることは対して難しいことではありません。後述しますが、これは適当に繋げれば良いので。

この例で一番難しい点は、 writeln(message);writeln って関数はどこから来たの?ということです。これを実行したいなら、import std.stdio; であるとか、 import std; という記述が必要のはずです。しかし何も考えずに補うのはちょっとナンセンスですね。

ではどうしようか?ということで、流石にこればっかりは書いてもらうスタイルでいきます。

つまり、

import std.stdio; // ここに書いてもらう
auto message = "Hello, world!";

writeln(message); // Hello, world!

です。

これはC#やJavaを書いてる人にはちょっと違和感あるかもしれませんが、D言語を書く人には何ら違和感のないポイントかと思います。

何故かと言えば…つなげて…こうじゃ!

分割したコードブロックを結合して動かす

void main() {
import std.stdio; // ここに書いてもらう
auto message = "Hello, world!";
writeln(message); // Hello, world!
}

はい。 これで動く からです。

インデントが無くて気持ち悪い! は置いといて、「import 文が任意のスコープに書ける」という言語仕様がここで光ります。計画通り…!

実際ツールにすればこのコードを目にすることはありませんのでインデントは無視です。意味も変わりませんし、これで動くのは逆にメリット と考えましょう。Python大変そう。

つまり繋げさえすれば、main を補うだけのシンプル設計です。わかりやすい。ここまで単純で済むのは最高ですね。

コードブロックの結合範囲

ここが一番の目玉かもしれません。

忘れてはいけないのが、ここまで意味や役割の違う3つのコードブロックのセットが出てきた、ということです。

これらを単に全部つなげたらそれは動きません。 main 関数の重複あたりが問題になります。
なので、コードブロックに対して 「これとこれは結合する」と指示する 必要があります。

そこで使うのが、Markdownのコードブロックにある「情報文字列」という仕様 です。

情報文字列

https://spec.commonmark.org/0.30/#info-string

私もツールを作るまで知らなかったのですが、 Markdownのコードブロックには好きな情報を付与することができます。

この仕様を使って以下の要領でコードブロックを書くことにより名前付けを表現、あとは同じ名前で出現順序通りに結合して1つのソースとして実行します。

concat_exampleという名前で結合範囲を決める例

```d name=concat_example
import std.stdio; // ここに書いてもらう
auto message = "Hello, world!";
```

と

```d name=concat_example
writeln(message); // Hello, world!
```

言語名のあと、スペース1つを区切りとしてそれ以降がすべて1つの「情報文字列」となります。この場合、name=concat_example がコードブロックの名前、つまり結合範囲の指示ということです。

ここにはその昔JSONを書いたりすることが想定されていたようですが、今はスペース区切りで好きに書くスタイルが主流のようです。

```d foo=a bar=b
```

という感じで、気持ちHTMLっぽいですかね?

と、ここまで話が整えばツールはほとんどできたようなものです。

ツールの実行

作りました!の詳細は後編でやろうと思うので、今回は実行部分をやっていきます。

今回Markdownの拡張子からその名前を取って md というツールを作りました。
D言語のパッケージマネージャーである dub に登録してあります。

https://github.com/lempiji/md

パッケージマネージャーで直接実行できるので、たとえば README.md を実行するなら以下のようになります。

dub run md -- README.md

はい。すごくMarkdownを実行したい感が表現できた気がしており満足です。dl とかは取られているので、ここまで単純な名前取れたのは幸いですね。

実行結果

実行結果も比較的単純な例を出しておきます。

当たり前ですが、CI用にすべて成功したかどうかでリターンコードを分けています。
すべて成功すればリターンコードは 0 で、エラーがあれば 1 です。

ちなみに標準出力にそれらしき内容表示しています。

成功時

begin: main
Hello, world!
end: main
begin: example1
end: example1
Total blocks: 2
Success all blocks.

失敗時

begin: main
Hello, world!
end: main
begin: example1
C:\Users\user\AppData\Local\Temp\.md\md_985AAF3CFFA4C0440A9D8CB21D409CE7.d(7,5): Error: static assert:  `false` is false
C:\D\dmd2\windows\bin64\dmd.exe failed with exit code 1.
end: example1
Total blocks: 2
Errors: 1
Program exited with code 1

あとはCI組むだけですね。

ツールを使ってCIを組む

社内ドキュメントならお金を払うなりして好きにCIを組むことができますが、ネットの技術記事に無料でCIを付けたいと思ったら選択肢は絞られます。

パッと浮かぶもので、GitHubの GitHub Actions や CircleCI でしょうか。
GitHubならソースコードとしてMarkdownを保管するのと一緒にCIが設定できそうですね。

あー、そういえば、どこかの技術記事を公開するサービスでは、このGitHubと連携できたような…
たしか Z で始まるなにがしのサービスで…

そう!ZennにはGitHub連携というのがありましたね!!

これは 「記事をMarkdownで書いてリポジトリに配置すれば自動で公開して同期までやってくれる」 という大変便利な機能です。
詳しくは以下の記事などご参照ください。

https://zenn.dev/zenn/articles/setup-zenn-github-with-export

というわけで今回は、記事公開のために用意したリポジトリでCIもやってしまおう という方向で進めます。記事の管理も楽になりCIもできる、まさに一石二鳥というわけです。

そして本記事の公開に合わせて原本のリポジトリも公開しています。何か気になる点があれば参照してみてください。

https://github.com/lempiji/zenn-content

※ 今のところリポジトリが2つ連携できるので、こういった実験用で切り分けられるのはありがたいですね。まだ1つしか連携していませんが。

CIを設定する

GitHub ActionsでD言語のCIをサクッと書く方法です。

GitHub Actionsでは、リポジトリ内に .yml ファイルにテスト方法を書いて指示します。

ここでのポイントは dlang-community/setup-dlang というコンパイラセットアップ部分と schedule とある日時のトリガーです。
あとは先の実行コマンドだけですね。

以下、この記事をテストしている最小限のymlを記載します。あとはファイル名を変えれば、これをリポジトリのルートから見て、 /.github/workflows/test.yml として置くだけでOKです。

test.yml
name: qiita-adc-d-20211124
on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]
  schedule:
    - cron:  '0 15 * * *'

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
    - uses: dlang-community/setup-dlang@v1
    - name: 'Test Markdown'
      run: |
        dub run md --compiler=$DC -- articles/qiita-adc-d-20211224.md

cron というのが時刻指定で、これは毎日24時を目途に実行するサンプルです。
詳しい記法は調べてみてください。

(あと、せっかくだしこれも文法くらいはテストされたらなぁ、と思ったりします)

バッジをドキュメントに埋め込む

ここまで来たらバッジのURLを取ってきて貼るだけです。

リポジトリにアクセスして Actions のタブに移動、Create Status Badgeのメニューを押します。

あとはコピーボタンを押せばコピーされるので、ドキュメントの要所に埋め込むだけです。簡単ですね!

他の連携例

最後にこれに近いCIを組んでいる自作ライブラリのリポジトリを2つ紹介しておきます。

1つ目は、作ったツールである md の README.md です。
機能を説明しつつ、ツールそれ自体の機能を確認するコードにもなる。これぞ一石二鳥です。

https://github.com/lempiji/md

2つ目は、自作の計算グラフライブラリで用意したサンプル多めの README.md です。
README.md だとユースケースを考えて書くので、日頃のテストとはまた違った観点で補うコードになります。これも良き。

https://github.com/lempiji/golem

さいごに

今回作った md ですが、元々自作ライブラリのREADMEにサンプルをたくさん書いてみるチャレンジをしていまして、その内容が正しいか確認するために作ったものでした。

振り返ってみると以下のような点でメリットがあったように思います。

  • README.md に書かれたサンプルが単体テストになったおかげで何度かバグが防げた
  • 思ったより誤字脱字があり、それがすべて洗い出せた
  • CIでドキュメントまでチェックされている安心感が得られた

いろいろありましたが、全体的な感想を言えば「思い付き駆動でアイデアを形にするのはとても楽しい」です。
言語的な特徴とも合致して、パズルがバシバシはまっていく感覚を久しぶりに味わうことができました。

ただ、近い将来役目を終えた記事がたくさん出ることで「古くとも価値ある記事が埋もれる可能性」というのは、調べものが多い立場としては結構危機的に思えます。
Markdownを見てよろしく実行してくれる何か、文章校正ツールなどなど、今後も「Markdownを検証する未来」というのは色々模索してみたいと思います。

後編では、ツールの機能とハマりどころや制限、実装の振り返りをもう少し話します。

以上です。

GitHubで編集を提案

Discussion