技術メモ

技術メモ

ラフなメモ

Qiisync開発雑記

はじめに

Qiita の記事を CLI で投稿したり、更新することができるクライアントツールを作りました。

https://github.com/d-tsuji/qiisync

機能的な紹介は README が詳しいので、このブログでは開発に関する雑記を記したいと思います。

CLIの設計

ユーザからどのようなパラメータを受け取ってどのような処理を実現するか?という点です。Qiisync を作っていて一番気になったのが、記事を投稿するコマンドオプションをどのように入力させるか、という点です。

記事を投稿するためには本文のマークダウンの他に、タイトル、タグ、限定公開記事にするかどうか?という 3 点を少なくとも指定する必要があります。これらをコマンドラインのオプションで指定するのか、標準入力からインタラクティブに受け取るのか、迷いました。

3 つも( --private はデフォルトで false にしておけば 2 つ)必須で入力させるのはどうかな?初見のユーザでちょっと使いにくいかもしれない...と思い、今回は標準入力からインタラクティブに受け取る方針にしています。

qiisync post <filepath>

コマンドラインからパラメータを受け取る場合は以下のように考えていました。

qiisync post <filepath> --title xxx --tag Go:1.14 --private false

ただ、標準入力からインタラクティブに受け取るようにすると、いちいち入力を求められるので、それはそれで面倒、というユーザもいると思っていて、このあたりの感覚はまだつかめていないです。

フォルダ構成

最初は main.go も含めてすべてのファイルを main パッケージとして実装していました。がまぁ、起動のエントリーポイントと実装の詳細は分離したほうがいいだろう、と思い直し、コマンドの実装の詳細は qiisync パッケージとして切り出しました。main.go に関しては ./cmd/qiisync/main.go としてよくある cmd フォルダ配下に格納しました。これはよく馴染んでいると思っています。

以下のような構成です。

.
├── article.go
├── article_test.go
├── cmd
│   └── qiisync
│       └── main.go
...

テスト

基本的に Qiita が提供している API を用いて処理を組み立てることになるので、API のコールは Qiisync の中心的な役割になります。単体テストはしっかりやっておきたいな、と思っていて一番しっくりきたのが go-github の実装です。Qiisync の HTTP クライアントの実装方法は go-github を参考にしています。

以下のように構造体にリクエストの URL を保持しておいて、リクエストの引数としてパス文字列を受け取る方針です。

func (b *Broker) NewRequest(method, urlStr string, body interface{}) (*http.Request, error) {
    if !strings.HasSuffix(b.BaseURL.Path, "/") {
        return nil, fmt.Errorf("BaseURL must have a trailing slash, but %q does not", b.BaseURL)
    }
    
    // ...
}

このようにしておくと setup() 内で httptest によるモックサーバを立ち上げることができ、かつパスをマルチプレクサに登録でき、テスタブルな HTTP クライアントになります。

func Test_fetchRemoteArticle(t *testing.T) {
    broker, mux, _, teardown := setup()
    defer teardown()

    mux.HandleFunc("/api/v2/items/c686397e4a0f4f11683d", func(w http.ResponseWriter, r *http.Request) {
        testMethod(t, r, "GET")
        fmt.Fprint(w, `
                  {
                      "rendered_body": "<h1>Example</h1>",
                      "body": "# Example",
                      "coediting": false,
                  }
`)
    })

Makefile

まだカチッと Makefile が定まっていないのですが、だいたい普段使いするコマンドは決まるはずなので、もう少し悩まずに予め決めておきたいです。いまのところ以下のような Makefile になっています。

.PHONY: all build test lint clean deps devel-deps

BIN := qiisync
BUILD_LDFLAGS := "-s -w"
GOBIN ?= $(shell go env GOPATH)/bin
export GO111MODULE=on

all: clean build

deps:
    go mod tidy

devel-deps: deps
    GO111MODULE=off go get -u \
      golang.org/x/lint/golint

build:
    go build -ldflags=$(BUILD_LDFLAGS) -o $(BIN) ./cmd/qiisync

test: deps
    go test -v -count=1 ./...

test-cover: deps
    go test -v -count=1 ./... -cover -coverprofile=c.out
    go tool cover -html=c.out -o coverage.html

lint: devel-deps
    go vet ./...
    $(GOBIN)/golint -set_exit_status ./...

clean:
    rm -rf $(BIN)
    go clean

リリース

GoRelease がめちゃくちゃ便利です。.gorelease.yaml も少し悩んだので、テンプレ的な感じにしておきたいです。今回初めて Homebrew の設定を含めました。

project_name: qiisync
before:
  hooks:
    - go mod tidy
builds:
  -
    goos:
      - linux
      - darwin
      - windows
    goarch:
      - amd64
      - 386
    main: ./cmd/qiisync
    binary: qiisync
    ldflags:
      - -s -w
      - "-X main.version={{.Version}}"

archives:
  - format: zip
    name_template: "{{ .Binary }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}"
    files:
      - LICENSE
      - README.md

nfpms:
  -
    formats:
      - deb
      - rpm
    dependencies:
      - rpm
    vendor: "d-tsuji"
    homepage: "https://github.com/d-tsuji/qiisync"
    maintainer: "Tsuji Daishiro"
    description: "Qiita CLI tool, support push and pull from/to local filesystem and Qiita."
    license: "MIT"
    file_name_template: "{{.ProjectName}}_{{.Version}}_{{.Os}}-{{.Arch}}"
    replacements:
      amd64: 64-bit
      386: 32-bit
      darwin: macOS
      linux: Tux

brews:
  - description: "Qiita CLI tool, support push and pull from/to local filesystem and Qiita."
    github:
      owner: d-tsuji
      name: homebrew-qiisync
    commit_author:
      name: goreleaserbot
      email: goreleaser@carlosbecker.com
    homepage: "https://github.com/d-tsuji/qiisync"
    install: |
      bin.install "qiisync"
    test: |
      system "#{bin}/qiisync"

checksum:
  name_template: "{{ .ProjectName }}_{{ .Version }}_checksums.txt"

GitHub Actions

これもめちゃくちゃ便利。いまのところ push したときに起動するテスト用と、タグを打ったときに実行されるリリース用の 2 つの GitHub Actions を使っています。

  • テスト

テストではカバレッジも取得するようにしています。またテスト環境としては macOSubuntu で回しています。Windows でも回したいのですが、これは実装側を Windows 対応する必要があって、対応できていないです。

name: test
on: [push, pull_request]
jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        os:
          - ubuntu-latest
          - macOS-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@master
      - name: Setup Go
        uses: actions/setup-go@v1
        with:
          go-version: 1.x
      - name: Add $GOPATH/bin to $PATH
        run: echo "::add-path::$(go env GOPATH)/bin"
      - name: Test
        run: make test-cover
      - name: Lint
        run: make lint
      - name: Send coverage
        uses: shogo82148/actions-goveralls@v1
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          path-to-profile: c.out
          parallel: true
          job-number: ${{ strategy.job-index }}
  finish:
    runs-on: ubuntu-latest
    needs: test
    steps:
      - name: finish coverage report
        uses: shogo82148/actions-goveralls@v1
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          parallel-finished: true
  • リリース

ちょっとだけハマったのが、homebrew に登録するときに GitHubリポジトリを作成して、そのリポジトリにコミットするようにします。goreleaserでHomebrewのFormulaを自動生成する の内容の話です。最初、GITHUB_TOKEN に権限がないトークンを指定していて、書き込めないという事象でちょっと悩みました。これは Write 権限がある ACCESS_TOKEN を発行することで解決しました。

name: release
on:
  push:
    tags:
      - "v[0-9]+.[0-9]+.[0-9]+"
jobs:
  release:
    runs-on: ubuntu-latest
    steps:
    - name: setup go
      uses: actions/setup-go@v1
      with:
        go-version: 1.14.x
    - name: checkout
      uses: actions/checkout@v2
    - name: run GoReleaser
      uses: goreleaser/goreleaser-action@v1
      env:
        GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
      with:
        version: latest
        args: release --rm-dist

まとめ

CLI ツールの作成は楽しいです。私にとって少し便利になるツールを作りました。これが少しでも誰かの役に立つと嬉しいです。