Plan 9とGo言語のブログ

主にPlan 9やGo言語の日々気づいたことを書きます。

Plan 9とInfernoにおけるtar(1)の変化

小ネタです。以下の記事を読んでいて、

なぜ不要なのかは元記事を読んでもらうといいのだけど、ここではPlan 9ではどうなのか気になったのでtar(1)を調べてみた。ベル研UNIXの子孫なので当然だろうけど、Plan 9のマニュアルでは key の存在がそのまま残っている。

tar key [ file ... ]

The key is a string that contains at most one function letter plus optional modifiers.

なんだけど、そこで終わりではなく、Plan 9から派生したInfernoでは tar(1) コマンドが無くなっていて、代わりにgettar(1)で置き換えられている。他にも puttar(1)lstar(1) があって、それぞれ tar x, tar c, tar t に相当する。もともと、tar(1)crtx はサブコマンドのようなものだと言われていたけど、Infernoで再定義する際にサブコマンドではなく別のコマンドとして整理したのは「一つのことをうまくやる」の現れなのかなと思った。

余談だけど、1つの文字に固有の意味があって、それらを並べて一連の文字列で表現するものはPlan 9にいくつか残っている。例えばパーミッションlarwx もそうだし、以前使われていたファイルサーバ専用カーネルではディスクの構成も1つの文字列で表現していた。具体的には h は(S)ATAディスクを、 wSCSIディスクを意味して、それに続く数字で「どのディスクなのか」を識別する。これを組み合わせて (w1w2w3) なら3つのディスクを単純に連結する意味になるし、[w1w2w3]{w1w2w3}RAID 0RAID 1相当の意味となっていた。初見だとむちゃくちゃ混乱するけど、慣れるとこれはこれで使い易いと思うのですよね。

Goでモンキーパッチするライブラリを作った

Goで単体テストを実装する場合、動的な言語のように「テスト実行中に外部への依存を置き換える」といったことはできません。代わりに、

のように、テスト対象をテスト可能な実装に変更しておき、テストの時は外部への依存をモック等に置き換えて実行する場合が多いのではないかと思います。

個人的な体験でいえば、テスト可能な実装に置き換えていく過程で設計が洗練されていく*1ことは度々あるので、面倒を強制されているというよりは設計を整理するための道具といった捉え方をしているのですが、そうは言っても動的な言語に比べると面倒だなと感じるときは少なからずあります。既存の実装がテスト可能になっておらず、変更するコストが高い場合は特にそうですね。

そんなとき、気軽にモンキーパッチできると嬉しいんじゃないかと思って、テストの時だけ関数を置き換えられるようなライブラリを作りました。

github.com

このライブラリはtenntenn/testtimeにとても影響を受けています。

使い方

試しに標準ライブラリの time.Now を置き換えます。具体的なコードは次のようになります。

import (
    "testing"
    "time"

    "github.lufia/plug"
)

func isLeap() bool {
    now := time.Now()
    return (now.Year() % 4) == 0 // 主題ではないのでうるう年の実装は省略
}

func TestIsLeap(t *testing.T) {
    scope := plug.CurrentScopeFor(t)
    key := plug.Func("time.Now", time.Now)
    plug.Set(scope, key, func() time.Time {
        return time.Date(2024, 5, 10, 11, 0, 0, 0, time.UTC)
    })
    if !isLeap() {
        t.Errorf("2024 is a leap year")
    }
}

plug.Func の第1引数は関数の名前を指定します。理由は後述しますが、これは必ず以下の書式で記述してください。

  • (package-path).(function-name)
  • (package-path).(type-name).(method-name)

例を挙げると math/rand/v2.Nnet/http.Client.Do などです。標準の go doc が受け取る引数と似せていますが、パッケージ名の省略はできません*2

これで、TestIsLeap の中で実行した time.Now は固定で2024年5月10日の時刻を返すようになります。スタックを抜けない限り影響は続くので、isLeap 関数が呼び出す time.Time も固定の値を返します。

テストの実行

テストを実行するときは以下のように実行してください。-overlayオプションが必要です。

go test -overlay <(go run github.com/lufia/plug/cmd/plug@latest)

# 分けて書いてもいい
go run github.com/lufia/plug/cmd/plug@latest >overlay.json
go test -overlay overlay.json

-overlay オプションの詳細は、上で挙げたtenntennさんの記事を読んでもらうと良いのですが、ここでは以下のようなことを実行しています。

  • カレントディレクトリのソースコードから plug.Func を探す
  • plug.Func の第2引数を動的に置き換えできるように書き換える
  • 実行スタックに関連づいたスコープを抜けるまで、plug.Set の第3引数に渡す関数で time.Now を置き換える
  • time.Now を呼び出したとき、実行スタックを遡って直近の time.Now を呼び出し、結果を返す
  • 該当する関数が実行スタック上で置き換えられてなければ本物の結果を返す

plug@latest はカレントディレクトリに plug/ というディレクトリを作成しますが、これは実行するたびに生成するので、不要になったら消しても問題ありません。

サブテストで部分的に置き換える

一部のサブテスト実行中だけ、別の値に置き換えたい場合は、サブテストで同じように書くと実現できます。

func TestIsLeap(t *testing.T) {
    scope := plug.CurrentScopeFor(t)
    key := plug.Func("time.Now", time.Now)
    plug.Set(scope, key, func() time.Time {
        return time.Date(2024, 5, 10, 11, 0, 0, 0, time.UTC)
    })
    t.Run("サブテスト", func(t *testing.T) {
        scope := plug.CurrentScopeFor(t)
        plug.Set(scope, key, func() time.Time { ... })
        // これ以降、サブテストの中では別の値を返す
    })
    // サブテストの外では2024年5月の時刻を返す
}

メソッドを置き換える

メソッドも置き換えできます。以下の例では、net/http.ClientDo メソッドを置き換えているので、http.Get にも影響しています。

func TestHTTPClientGet(t *testing.T) {
    scope := plug.CurrentScopeFor(t)
    key := plug.Func("net/http.Client.Do", (*http.Client)(nil).Do)
    plug.Set(scope, key, func(req *http.Request) (*http.Response, error) {
        return &http.Response{StatusCode: 200}, nil
    })
    resp, _ := http.Get("https://example.com")
}

ジェネリック関数を置き換える

型パラメータのある関数は、型ごとに関数を渡します。

func TestMathRand(t *testing.T) {
    scope := plug.CurrentScopeFor(t)
    key := plug.Func("math/rand/v2.N", rand.N[int])
    plug.Set(scope, key, func(n int) int {
        return 3
    })
    fmt.Println(rand.N[int](10))
}

このとき、 rand.N[int]plug.Set で差し替わった関数が使われますが、 rand.N[int64] は登録していないので本物の実装が使われます。

関数の引数や呼び出し回数を検査する

内部的に呼ばれた回数を持っているので、それを使って期待した通りに呼ばれているかを検査できます。plug.FuncRecorder[T] に渡す構造体のフィールドは、関数引数の名前に対応したものが使われます。このとき、関数引数の名前をブランク指定子(_)にしていると無視します。

func TestRecorder(t *testing.T) {
    scope := plug.CurrentScopeFor(t)
    key := plug.Func("os.Getenv", func(string) string {
        return "dummy"
    })
    var r plug.FuncRecorder[struct {
        Key string `plug:"key"`
    }]
    plug.Set(scope, key, fake).SetRecorder(&r)

    os.Getenv("PATH")
    if r.Count() != 1 {
        t.Errorf("Count = %d; want 1", r.Count())
    }
    if r.At(0).Key != "PATH" {
        t.Errorf("At(0).Key = %s; want PATH", r.At(0).Key)
    }
}

今後の予定

実行のたびに静的解析をして必要なファイルを生成しているので、パッケージが多くなってくると有意に遅くなります。Goツールチェーンとパッケージのバージョンが変わらなければ基本的には生成するファイルも同じものになるので、うまく最適化ができるといいですね。

他にも、ジェネリック型のメソッドに対応したりとか、go build でも使えるようにしたりなど、色々とやりたいことはあります。

捕捉: なぜ文字列のキーを必要としているか

Goでは関数が同一かどうかを比較することができません。ジェネリックでない関数の場合は reflect.ValueOf(os.Getenv).Pointer() を経由することで比較できますし、Linux/AMD64の場合はだいたい期待通りに動きますが reflect.Value.Pointer のドキュメントには以下のように書かれています。

If v's Kind is Func, the returned pointer is an underlying code pointer, but not necessarily enough to identify a single function uniquely. The only guarantee is that the result is zero if and only if v is a nil func Value.

さらにジェネリック関数では、型パラメータごとに異なる関数ポインタが割り当てられるようで、 reflect.Value.Pointer での比較にも失敗します。

func N[T any](n T) {}

func N1[T any](n T) func(T) {
    return N[T]
}

func N2[T any](n T) func(T) {
    return N[T]
}

func main() {
    fmt.Println(reflect.ValueOf(N[int]).Pointer() == reflect.ValueOf(N[int]).Pointer())   // true
    fmt.Println(reflect.ValueOf(N1[int]).Pointer() == reflect.ValueOf(N[int]).Pointer())  // false
    fmt.Println(reflect.ValueOf(N2[int]).Pointer() == reflect.ValueOf(N2[int]).Pointer()) // true
}

runtime.FuncForPC なども含めて色々と試してみたけれど、Go 1.22時点では良い方法がなかったので、今の形に落ち着きました。

*1:テストコードと同様にドキュメントを書いているときにもよく起きる

*2:go docは http.Client と記述すると推測してくれる

Go製バイナリを配布するためのGitHubワークフロー

前置き

以前、BuildInfoからバージョンを取得する方法を紹介しました。

blog.lufia.org

go installで正規の公開されたバージョンをインストールした場合は、以下の出力においてmodの行が示すように、sum.golang.orgチェックサム等が検証されてバイナリのメタデータに埋め込まれます。

$ go version -m dotsync
dotsync: go1.22.2
    path    github.com/lufia/dotsync
    mod github.com/lufia/dotsync    v0.0.2  h1:JWm92Aw8pSKJ4eHiQZIsE/4rgwk3h5CjEbJ/S30wiOU=
    build   -buildmode=exe
    build   -compiler=gc
    build   -trimpath=true
    build   DefaultGODEBUG=httplaxcontentlength=1,httpmuxgo121=1,panicnil=1,tls10server=1,tlsrsakex=1,tlsunsafeekm=1
    build   CGO_ENABLED=0
    build   GOARCH=amd64
    build   GOOS=linux
    build   GOAMD64=v1

上記の出力から「dotsyncのバージョン0.0.2をgo1.22.2でビルドした」ことを読み取れますね。チェックサムhttps://sum.golang.org/lookup/<module-path>@<version> のようなURLにアクセスすると、正規のものかどうかの確認を行えます。完全なURLの仕様はGo Modules Reference/Checksum databaseをみてください。

このチェックサムが一度でも登録されてしまった後は、消したり変更したりできません。消せないのは困ると思うかもしれませんが、proxy.golang.org

I removed a bad release from my repository but it still appears in the mirror, what should I do?

への回答があるので、不備などではなく意図してデザインされていることが読み取れます。削除はできないものの、Goで非推奨(Deprecated)や撤回(Retracted)を明示する方法のようにすれば意思を表明することは可能です。

正しくソースコードからビルドされたことを検証する

主に以下の条件を満たす*1場合、Go 1.21以降では生成するバイナリが完全に一致するので、同じパラメータを与えて手元でビルドしてみると検証できます。

  • Goコンパイラのバージョンが同じ
  • GOOS, GOARCH, GOAMD64 などターゲットが同じ
  • cgoを使わない
  • os/user, netなどで動的リンクをしない
  • ビルドするディレクトリ名が同じ、または-trimpathオプションを与える

以下の例はlegoコマンドをLinuxPlan 9でビルドしたものですが、同じハッシュ値になっている様子が分かると思います。

Linuxでビルド

$ go version
go version go1.22.2 linux/amd64

$ export GOTOOLCHAIN=go1.22.1
$ export GOOS=plan9
$ export GOARCH=amd64
$ export GOAMD64=v1
$ export CGO_ENABLED=0
$ go install -trimpath github.com/go-acme/lego/v4/cmd/lego@v4.16.1

$ sha1sum lego
ee2e9c121604c1f52cb53c0d0824288d772de1e7

Plan 9でビルド

% go version
go version go1.22.1 plan9/386

% GOTOOLCHAIN=go1.22.1
% GOOS=plan9
% GOARCH=amd64
% GOAMD64=v1
% CGO_ENABLED=0
% go install -trimpath github.com/go-acme/lego/v4/cmd/lego@v4.16.1

% sha1sum lego
ee2e9c121604c1f52cb53c0d0824288d772de1e7

再現可能なビルド

このように、第三者が特定のソースコードから生成されたものであると検証できるような概念は「再現可能なビルド」とか「再現性のあるビルド」と呼ばれるようです。

また、Go 1.21以降は、Goのコンパイラやライブラリも再現可能になっているようです。

コード署名とは違うのか

コード署名は、誰がビルドしたものなのかを検証できますが、特定のソースコードから生成されたものかどうかは保証しません。例えばビルドプロセスの途中で改ざんが行われた場合、コードの署名は正しく検証を通ってしまいます。

go buildの場合は正規のバージョンが入らない

ようやく本題です。この記事の冒頭で挙げたエントリでも書いたように、手元にソースコードを置いてgo buildした場合などでは、メインモジュールのバージョンやチェックサムが埋め込まれずに (devel) という文字列になります。

$ go version -m dotsync
dotsync: go1.22.2
    path    github.com/lufia/dotsync
    mod github.com/lufia/dotsync    (devel) 
    ...

(devel) の代わりに(おそらくv1.0.1-0.20240418xxxのような)疑似バージョンを埋め込む提案がcmd/go: stamp the pseudo-version in builds generated by go buildで承認されていますが、それでも正規のバージョンとは区別されていますし、少なくともGo 1.22の時点ではまだ実装されていません。

GoReleaserでビルドしたバイナリはメインモジュールのチェックサムを持たない

上記と同様に、2024年5月時点では、GoReleaserGoReleaser Actionでビルドしたバイナリは公開された正規なバージョンを持ちません。

実用上は致命的に困るものではないけれど、どうせなら検証可能になっていたほうが嬉しいですね。

GitHub Releasesでリリースしたらビルドして成果物に追加するワークフロー

というわけで、正規のバージョンが埋め込まれたバイナリをリリースするためのワークフローを作ってみました。以下のワークフローは、GitHub Releasesで新しいバージョンをPublishすると開始して、最終的にバイナリをリリースに添付します。

name: Release

on:
  release:
    types:
    - published
jobs:
  release:
    strategy:
      matrix:
        os:
        - linux
        - darwin
        - windows
        arch:
        - amd64
        - arm64
        include:
        - format: tgz
        - os: windows
          format: zip
        exclude:
        - os: darwin
          arch: amd64
        - os: windows
          arch: arm64
    runs-on: ubuntu-22.04
    steps:
    - uses: actions/checkout@v4
    - uses: actions/setup-go@v5
      with:
        go-version: stable
    - name: Build the package
      uses: lufia/workflows/.github/actions/go-install@v0.4.0
      with:
        package-path: github.com/lufia/dotsync
        version: ${{ github.ref_name }}
      env:
        GOOS: ${{ matrix.os }}
        GOARCH: ${{ matrix.arch }}
        CGO_ENABLED: 0
      id: build
    - name: Create the asset consists of the build artifacts
      uses: lufia/workflows/.github/actions/upload-asset@v0.4.0
      with:
        tag: ${{ github.ref_name }}
        path: >
          ${{ steps.build.outputs.target }}
          LICENSE
          README.md
        name: dotsync-${{ github.ref_name }}.${{ matrix.os }}-${{ matrix.arch }}
        format: ${{ matrix.format }}

  upload:
    needs: release
    permissions:
      contents: write
    runs-on: ubuntu-22.04
    steps:
    - uses: actions/download-artifact@v4
      with:
        path: assets
        merge-multiple: true
    - name: Upload the assets to the release
      run: gh release upload -R "$GITHUB_REPOSITORY" "$GITHUB_REF_NAME" assets/*
      env:
        GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

どうでしょうか。記述量は多いですが、1/3くらいはビルド用のマトリクスを作っているところなので、第一印象ほど複雑ではないかなと思います。

ワークフローの途中で読んでいる複合アクションは以下の2つなので、興味があれば眺めてみてください。

*1:他にもあるかもしれないけど、これだけ揃えればだいたい同じになるはず

Steamクライアントが起動しなくなっていた

2024年2月ごろにSteamクライアントを更新してから、以下のログで停止して起動しなくなっていました。

$ flatpak run com.valvesoftware.Steam
...
Steam Runtime Launch Service: starting steam-runtime-launcher-service
Steam Runtime Launch Service: steam-runtime-launcher-service is running pid 34081
bus_name=com.steampowered.PressureVessel.LaunchAlongsideSteam

Steam is killed with no error message when steam runtime launch service is ranによると、落ちている原因はSteamにbackgroundパーミッションを与えていないからのようなので、以下のコマンドで追加すると起動できるようになります。

$ flatpak permission-set background background com.valvesoftware.Steam yes

ここでbackgroundが2つ並んでいるのは正しくて、最初がパーミッションストアのテーブル名、2つ目がパーミッションストアのオブジェクトの名前です。

以下のような結果になっていれば動作します。

$ flatpak permission-show com.valvesoftware.Steam
Table      Object     App                     Permissions                  Data
background background com.valvesoftware.Steam yes                          0x00

Titan Security Keyの新しいバージョンがPasskeysに対応していた

Titan Security Keyがパスキーに対応していたようですが、Google Storeの製品ページを見ても、それがパスキー対応したバージョンのTitan Security Keyなのか分からなくて混乱しました*1。ストア上では全部同じ名前でバージョン表記も無いし、国内販売が遅れた事例も過去にあるので「国内版はまだ古いバージョンだったりしないか」が気になります。

そこで、過去の記事などから形状と対応規格を調べてみました。過去バージョンは公式のGoogle Titan セキュリティ キーの安全および保証ガイドを参照しています。

先に結論を述べると、2024年1月時点で販売されているTitan Security Keyはパスキー対応と謳われているバージョンです。

2018年

  • Model: K9T (USB-A/NFC)
  • Model: K13T (Micro-USB/NFC/BLE)

Titan Security Keys: Now available on the Google Storeで登場したバージョンです。購入したパッケージに2つ同梱されていました。もともと2018年に登場していましたが、国内販売が遅れて2019年販売開始でした。過去記事で買ったのはこれ。

blog.lufia.org

具体的な形状はGoogleの安全な2段階認証を構築し不正アクセスを防ぐ物理キー「Titan セキュリティ キー」が日本で登場 - GIGAZINEの写真を見てもらえれば分かりますが、K13Tは卵形をしていて中央に楕円のボタンがあります。

2019年

  • Model: YT1 (USB-C)

USB-C Titan Security Keys - available tomorrow in the US でUSB-C版が追加になりました。こちらも形状はGoogleの2段階認証を構築する「Titan セキュリティ キー」にUSB Type-Cタイプが登場 - GIGAZINEの写真を見てもらえれば分かりますが、円形の鍵穴とTITANの刻印があります。

ところで、Titan Security Key - Wikipediaには

USB-C/NFC

と書かれているけど、YT1の安全および保証ガイドを見るとNFCは含まれていないので間違いかなと思っています。

2021年

  • Model: K40T (USB-C/NFC)

この辺りはよく知らないのですが、おそらくSimplifying Titan Security Key options for our usersの辺りでアップデートされたバージョンです。接続インターフェイスとしてのBLEが廃止となる代わりにNFCが追加になったようです。

K40Tの画像はGoogle「Titan セキュリティキー」ってどう使うの?NFCによるスマホ利用にも対応を見てください。円形の鍵穴と丸いボタンがあります。

これも、Titan Security Key - Wikipediaによると

supporting U2F and FIDO2

とありますが、Google USB-C/NFC Titan Security Key ReviewではFIDO U2F対応(FIDO2ではない)とあるので間違いかなと思っています。

2023年

  • Model: K51T (USB-A/NFC)
  • Model: K52T (USB-C/NFC)

The latest Titan Security Key is in the Google Storeで更新されたバージョンで、現在販売されているモデルです。公式情報にFIDO2対応とあるので本物でしょう。

形状は、ストアの画像はおそらく次のバージョンが出たら変わってしまうので、その場合は最大250種類のパスキーの保存が可能なGoogle Titan セキュリティ キーを使ってパスキー認証してみた - GIGAZINEの写真をみてください。楕円形の鍵穴と四角のボタンがあります。

*1:本当に困るので更新日くらいは書いてほしい

Goで非推奨(Deprecated)や撤回(Retracted)を明示する方法

最近のGoには、関数やパッケージを非推奨と扱う方法があります。まとまっていると便利かなと思うので、種類ごとにまとめてみました。GoDocコメントを多用するので、GoDocを書き慣れていない場合は以下も参考にしてください。

blog.lufia.org

関数と型を非推奨にする

関数コメントに、// Deprecated: ではじまる段落を追加します。

// Parse parses a string of the form <status>=<status>.
//
// Deprecated: Use ParseStatusMap instead.
func Parse(src string) (map[Status]Status, error) {
    ...
}

型の場合も同様に。

// Error is the interface that wraps Error method. 
//
/// Deprecated: Use error instead.
type Error interface {
    ...
}

これを含んだバージョンをリリースすると、GoDocでは以下のように表示されます。

関数名と並んでDEPRECATEDと描画されている様子

ただし、関数や型の非推奨化を検出するLinterは存在します*1が、標準のツールでは警告等を出力しません。これはおそらく、非推奨だとしても互換性を維持するために関数は残り続ける習慣があるので、使い続けても支障はないためじゃないかなと思っています。

互換性を維持しつつ変更を加える方法は、以下の記事が参考になります。

変数と定数を非推奨にする

これも // Deprecated: コメントを追加します。

// ZP is the zero Point.
//
// Deprecated: Use a literal image.Point{} instead.
var ZP Point

定数も同様。

const (
    ...
    // Deprecated: Use TypeReg instead.
    TypeRegA = '\x00'
    ...
)

定数をまとめて非推奨にしたい場合はconstの前に書きます。

// Seek whence values.
//
// Deprecated: Use io.SeekStart, io.SeekCurrent, and io.SeekEnd.
const (
    SEEK_SET int = 0 // seek relative to the origin of the file
    SEEK_CUR int = 1 // seek relative to the current offset
    SEEK_END int = 2 // seek relative to the end
)

2023年現在、変数または定数の場合は、GoDocの上では特別な表示をしていません。

構造体のフィールドを非推奨にする

型そのものではなく、一部のフィールドだけ非推奨としたい場合は、該当フィールドのコメントに // Deprecated: ではじまる段落を追加します。

type FileHeader struct {
    ...
    // ModifiedTime is an MS-DOS-encoded time.
    //
    // Deprecated: Use Modified instead.
    ModifiedTime uint16
    ...
}

行コメントの場合はこのように。

// PipeNode holds a pipeline with optional declaration
type PipeNode struct {
    ...
    Line     int             // The line number in the input. Deprecated: Kept for compatibility.
    ...
}

2023年現在、GoDocの上では特別な表示をしていません。

モジュールの特定バージョンを撤回する

壊れた状態でリリースしたしまったとか、関数名をtypoしていた等でバージョンを撤回したい場合があると思います。この場合は、go.modretractディレクティブを追加して、新しいバージョンを公開します。

retract v0.1.0 // Contains a misleading function name.

retractディレクティブでバージョンを示すと、モジュール上では撤回されたバージョンとして扱われます。GoDocでの表示はこのようになります。

撤回されたフラグが描画されている様子

また、撤回されたバージョンを参照したとき、goコマンドによって以下のような警告が出力されます。

go: warning: github.com/mackerelio/checkers@v0.1.0: retracted by module author: Contains a misleading function name.

retract ディレクティブは複数のバージョンをまとめて撤回したりもできます。詳細はドキュメントを参照してください。

モジュールそのものを非推奨にする

新しいメジャーバージョンを公開したので古いほうを非推奨としたい、またはメンテナンスを縮小するので非推奨としたい場合にはモジュールそのものを非推奨とすることもできます。

この場合は、go.modmoduleディレクティブに // Deprecated: を書きます。

// Deprecated: Use github.com/gomodule/redigo instead.
module github.com/garyburd/redigo

GoDocでの表示はこのようになります。

モジュールは非推奨と警告を出している様子

公式のドキュメントは以下です。

*1:staticcheck

GitHub ProjectsのTracksとTracked byを設定する

GitHub ProjectsにはTracksフィールドとTracked byフィールドがあります。

フィールド選択のところで確認できます

これらのフィールドは、新しいタスクリストでissueやプルリクエストを追加すると追跡の対象となります。

```[tasklist]
## Tasks
- [ ] #1234
- [ ] #1233
```

こうすると、タスクリストを記述した側のissueは#1234#1233を追跡している(Tracks)ことになり、反対に#1234#1233は親のissueに追跡された状態(Tracked by)となります。複数の親issueから1つの子issueを追跡することも可能です。この場合、2つのissueから参照された子issueは、2つのTracksを持ちます。

従来のタスクリストを使ってもUI上では追跡されているようにみえますが、どうやら従来のタスクリストはTracksTracked byの対象となっていません。

追跡されているようにみえるけど対象にならない

GitHub Projectsのタスクリストと追跡については公式ドキュメントがあります。

GitHub上のタスクリスト

2023年8月現在、GitHubのissueやプルリクエストでタスクリストを作成するには2種類の方法があります。

1つ目は従来からあるタスクリストです。以下のように記述します。

- [ ] task
- [ ] task

この方法は、日本語版のドキュメントではタスクリスト、英語版のドキュメントではtask listsと表現されています。

もうひとつ、昨年あたりに追加された方法があります。新しいタスクリストの書き方はこのようになります。

```[tasklist]
## section
- [ ] task
- [ ] task
```

こちらの方法は、日本語版だと従来と同じタスクリストと呼ばれていますが、英語版ではtasklistsと使い分けがされているようです。

新しいタスクリストがただのコードブロックになる場合

新しいタスクリストは、ドキュメントによるとまだプライベートベータです。Organization単位で有効化されるようなので、タスクリストがコードブロックになる場合は、該当のOrganizationはまだ無効なのかもしれません。この場合は、有効化したいOrganizationのAdminを持ったアカウントでWaitlistに登録しておくと、そのうち使えるようになります。

ただし、Waitlistに登録するフォームはOrganizationの指定が必須となっていたので、個人のアカウントはWaitlistに登録できません*1

*1:Feature Previewにも無かったので個人のリポジトリでは使えない気がする