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 を使っています。
- テスト
テストではカバレッジも取得するようにしています。またテスト環境としては macOS と ubuntu で回しています。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 ツールの作成は楽しいです。私にとって少し便利になるツールを作りました。これが少しでも誰かの役に立つと嬉しいです。