Node.js で Google Fit に体重データを登録する

先日 Node.js で Google Fit から歩数や体重データを取得するのをやりました。

blog.kondoumh.com

体重データはスマホアプリから登録できるけど Fit を使う前の過去データは手入力やってられてられないので、API による登録方法を調べました。

公式ドキュメントには記述が見つけられなくて API のヘッダーコメントのサンプルを頼りに試行錯誤が必要でした。

https://github.com/googleapis/google-api-nodejs-client/blob/master/src/apis/fitness/v1.ts

Python 実装ですがこのリポジトリが参考になりました。

github.com

GCP のプロジェクトで Fitness API を有効にしたり OAuth 2.0 クライアント ID を作成したり googleapis の NPM パッケージを使用したりするのは前回と同様です。

www.npmjs.com

まず体重データを登録するための DataSource を登録する必要があります。これには、 fitness.users.dataSources.create メソッドを使用します。dataType の定義が重要で、name 属性に com.google.weight を指定し、field 属性の配列にに体重データを格納するための name 属性 weightformat 属性 floatPoint を指定します。 devicemodelmanifacturer などはダミーデータで OK です。*1

async function createDataSource() {
  const res = await fitness.users.dataSources.create({
    userId: "me",
    requestBody: {
      "application": {
        name: "patch_weight",
        detailsUrl: 'https://example.com',
        version: "1"
      },
      "dataType": {
        name: "com.google.weight",
        field: [
          {
            name: "weight",
            format: "floatPoint"
          }
        ]
      },
      "dataStreamName": "patch_weight",
      "type": "raw",
      "device": {
        manufacturer: "mh",
        model: "hoge",
        type: "scale",
        uid: "pw-01",
        version: "1.0"
      }
    }
  });
  console.log(res.data);
}

この関数を、認証関数に続けて実行します。

authenticate(scopes)
  .then(createDataSource())
  .catch(console.error);

次のようなレスポンスが得られます。PROJECT_NO には Fitness API を有効化している GCP のプロジェクト番号が入ります。

{
  dataStreamId: 'raw:com.google.weight:PROJECT_NO:mh:hoge:pw-01:patch_weight',
  dataStreamName: 'patch_weight',
  type: 'raw',
  dataType: { name: 'com.google.weight', field: [ [Object] ] },
  device: {
    uid: 'pw-01',
    type: 'scale',
    version: '1.0',
    model: 'hoge',
    manufacturer: 'mh'
  },
  application: {
    version: '1',
    detailsUrl: 'https://example.com',
    name: 'patch_weight'
  },
  dataQualityStandard: []
}

この dataStreamId を指定して、体重データを登録していくことになります。

fitness.users.dataSources.datasets.patch メソッドを使用して体重データを登録する関数を定義します。

async function patch(dataSourceId, datasetId, start, end, time, val) {
  const res = await fitness.users.dataSources.datasets.patch({
    datasetId: datasetId,
    dataSourceId: dataSourceId,
    userId: "me",
    requestBody: {
      "dataSourceId": dataSourceId,
      "minStartTimeNs": start,
      "maxEndTimeNs": end,
      "point": [
        {
          dataTypeName: "com.google.weight",
          startTimeNanos: time,
          endTimeNanos: time,
          value: [
            {
              fpVal: val
            }
          ]
        }
      ]
    },
  });
  console.log(res.data);
}

時刻についてはなぜかナノ秒単位での指定が必要なので変換関数を用意し、特定日付で dataset を刻んで1件登録。測定時刻は午前7時としました。

function getNano(day) {
  const dt = new Date(day);
  return dt.getTime() * 1000000;
}

const dataSourceId = "raw:com.google.weight:PROJECT_NO:mh:hoge:pw-01:patch_weight";
const start = getNano("2020-12-31T00:00:00");
const end = getNano("2021-12-31T23:59:59");
const time = getNano("2020-12-31T07:00:00");
const datasetId = `${start}-${end}`;
const val = 69.3;

authenticate(scopes)
  .then(patch(dataSourceId, datasetId, start, end, time, val))
  .catch(console.error);

実行結果。エラーにはなりませんでした。

{
  minStartTimeNs: '1609340400000000000',
  maxEndTimeNs: '1640962799000000000',
  dataSourceId: 'raw:com.google.weight:PROJECT_NO:mh:hoge:pw-01:patch_weight',
  point: [
    {
      startTimeNanos: '1609365600000000000',
      endTimeNanos: '1609365600000000000',
      dataTypeName: 'com.google.weight',
      originDataSourceId: '',
      value: [Array]
    }
  ]
}

ちゃんとデータが入ったか確認するため、前回の歩数と体重データ取得関数で日付を指定して取得してみます。

const from = new Date("2020-12-31T00:00:00");
const to = new Date("2020-12-31T23:59:59");

authenticate(scopes)
  .then(client => aggregate(client, from, to))
  .catch(console.error);

実行結果。歩数は Pixel から、体重は API で登録した DataSource から取得され、他のデバイスと同等に集計されているようです。

raw:com.google.step_count.cumulative:Google:Pixel 3 XL:caa195e4620531ba:Step Counter
2020/12/31 15:05:19
2020/12/31 16:39:24
com.google.step_count.delta
3648

raw:com.google.weight:PROJECT_NO:mh:hoge:pw-01:patch_weight
2020/12/31 7:00:00
2020/12/31 7:00:00
com.google.weight.summary
69.3

ちなみに、Fit アプリで登録したデータはこのようにアプリでユーザの手入力とわかるようになっています。*2

raw:com.google.weight:com.google.android.apps.fitness:user_input
2021/7/23 8:58:26
2021/7/23 8:58:26
com.google.weight.summary
67.80000305175781

Pixel の Fit アプリでも反映まで少し時間がかかりましたが、API で登録したデータが確認できました。

f:id:kondoumh:20210726104907p:plain:w400

ということで、過去データも Fit に登録できるようになりました。

*1:電話や時計を持っているとそのデバイス専用の DataSource が登録されますが、更新はそのデバイスにしか許可されておらず、REST API で更新しようとすると認証エラーになります。

*2:Google Fit と連携可能な体重計の場合はここにデバイス名が入るはず。

Electron v14 リリースに備える

Electron v14 のリリースが 8/31 に迫っています。

www.electronjs.org

2年ぐらい前から12週ごとのリリースに移行しており、機能が deplicated になってから remove されるまでの期間も短くなっています。

www.electronjs.org

起動時に webPreferences で指定するオプションのデフォルト値もどんどんセキュア側に倒されていきます。

BrowserWindow | Electron

Electron 自体が Chrome のリリースに追従しているのでいたしかたないのだろうとは思います。

そして v14 ではとうとう remote module が削除されます。remote は renderer process から main process のオブジェクトをゴニョゴニョすることができる便利機能で Electron 関連の NPM ライブラリでもけっこう使われています。パフォーマンスやセキュリティの観点から非推奨となっていました。

simple-minds-think-alike.hatenablog.com

今後は、IPC 通信への移行が推奨されています。ライブラリ含めてマイグレーションするのはかなり大変だと推察されます。当面、Electron のバージョンを固定せざるを得ないアプリも出てくるのではないでしょうか。

Electron v14.0.0-beta.12 で野良 Scrapbox アプリを起動するとやはり Context menu が出なかったりバージョンダイアログがちゃんと表示されなかったりします。

Context menu は electron-context-menu の古いバージョンを使ってました。

www.npmjs.com

最新版では remote module への依存が排除されており、main process でしか動かないようになっています。それはいいのですが、野良 Scrapbox アプリでは WebView をタブに埋め込んでいるため、WebContents に対して Context menu を生成してもらいたいのですが、できなかったので、PR を送ってマージしてもらいました。

github.com

これで renderer process でやっていた context menu 構築を main process に持って行くことができました。

バージョンダイアログは about-window というパッケージを使ってました。

www.npmjs.com

こちらはまだ対応されていないため、ダイアログを自作しました。

f:id:kondoumh:20210715111013p:plain

あとは、enableRemoteModule オプションを無効化

remove enableRemoteModule option from webPreferences · kondoumh/sbe@9de8086 · GitHub

以上で、v14 が出ても大丈夫になりました。

Node.js で Google Fit のデータを取得する

今年になってから今更ですがドラクエウォーク始めました。

www.dragonquest.jp

通勤しないけど、このおかげでけっこう外歩きするようになってるので、歩数データと体重データの相関取ったりしてみたいなあと思いました。Google Fit って Web アプリは廃止されてて、スマホアプリの UI しかないんですが、REST API でデータを読み書きはできます。そこで REST API で Fit のデータを取得する方法を調べました。

Node.js で書きたいので googleapis の NPM パッケージを使うことに。

www.npmjs.com

API Key もいいんですが、よりセキュアな OAuth2 認証使うため local-auth パッケージもインストール

www.npmjs.com

OAuth2 を使って、Google の People API を呼び出すサンプルがありました。

https://github.com/googleapis/google-api-nodejs-client/blob/master/samples/oauth2.js

ローカル認証のために、localhost:3000 の http server を起動しリダイレクトして、oauth2Client から token を取得しています。 コマンドを叩くたびに、リダイレクト用ページがブラウザで開き、スマホにセキュリティ通知が来るのがうざいですがちゃんと動きます。

Fit の REST API を使うために、Google Cloud Platform のプロジェクトで Fitness API を有効化します。

https://console.cloud.google.com/apis/library/fitness.googleapis.com

認証情報で OAuth 2.0 クライアント ID を作成してダウンロードし oauth2.keys.json として保存して秘密のディレクトリに格納しておきます。

https://console.cloud.google.com/apis/credentials

Fitness REST API の Aggregate の仕様は以下で見れます。

Users.dataset: aggregate  |  Google Fit  |  Google Developers

歩数と体重を読むだけなので OAuth の認可スコープは、以下でよさそうです。

const scopes = [
  'https://www.googleapis.com/auth/fitness.activity.read',
  'https://www.googleapis.com/auth/fitness.body.read'
];

集計の軸は、dataTypeNamedataSourceId で指定します。ここにサンプルがありました。

Read the Daily Step Total  |  Google Fit  |  Google Developers

以下の指定でよいようです。

{
  "dataTypeName": "com.google.step_count.delta",
  "dataSourceId": "derived:com.google.step_count.delta:com.google.android.gms:estimated_steps"
}

Fit のアプリで登録した体重データは以下のように指定すれば取れました。

{
  "dataTypeName": "com.google.weight.summary",
  "dataSourceId": "derived:com.google.weight:com.google.android.gms:merge_weight"
}

ということで、このサンプルの runSample() 関数を書き換えます。

https://github.com/googleapis/google-api-nodejs-client/blob/master/samples/oauth2.js

async function runSample() {
  const res = await fitness.users.dataset.aggregate({
    userId: 'me',
    requestBody: {
      "aggregateBy": [
        {
          "dataTypeName": "com.google.step_count.delta",
          "dataSourceId": "derived:com.google.step_count.delta:com.google.android.gms:estimated_steps"
        },
        {
          "dataTypeName": "com.google.weight.summary",
          "dataSourceId": "derived:com.google.weight:com.google.android.gms:merge_weight"
        },
      ],
      "bucketByTime": { "durationMillis": 86400000 },
      "startTimeMillis": 1624658400000,
      "endTimeMillis": 1624698000000,
    },
  });

  const steps = res.data.bucket[0].dataset[0].point[0];
  console.log(steps.originDataSourceId);
  console.log(formatDate(steps.startTimeNanos));
  console.log(formatDate(steps.endTimeNanos));
  console.log(steps.dataTypeName);
  console.log(steps.value[0].intVal);

  const weight = res.data.bucket[0].dataset[1].point[0];
  console.log(weight.originDataSourceId);
  console.log(formatDate(weight.startTimeNanos));
  console.log(formatDate(weight.endTimeNanos));
  console.log(weight.dataTypeName);
  console.log(weight.value[0].fpVal);
}

function formatDate(timestamp) {
  let date = new Date();
  date.setTime(timestamp / 1000000);
  const params = {
    year: 'numeric', month: 'numeric', day: 'numeric',
    hour: 'numeric', minute: 'numeric', second: 'numeric',
    hour12: false
  };
  return date.toLocaleString("ja", params);
}

リクエストでは、取得したい期間 (startTimeMillis, endTimeMillis)をエポック時間で指定します。bucketByTime は集計に必要な期間を指定します (上記の read-daily-step-total サンプルに合わせてます)。userId は Google のアカウント名とかではなく me を指定します。

レスポンスの data.bucket[0].dataset は配列になっていて、歩数、体重の順に格納されてます。データの時刻はナノ秒単位で入っているので、フォーマットしました。実行すると以下のようにデータが取れます。

raw:com.google.step_count.cumulative:Google:Pixel 3 XL:caa195e4620531ba:Step Counter
2021/6/26 7:25:33
2021/6/26 15:22:25
com.google.step_count.delta
9422
raw:com.google.weight:com.google.android.apps.fitness:user_input
2021/6/26 8:02:26
2021/6/26 8:02:26
com.google.weight.summary
67.0999984741211

時期によっては、前使ってた Nexus 6P や Moto 360 のデータも入っていました。

Argo Workflow の機能と記法

前回デプロイと動作確認をしたので続きです。

blog.kondoumh.com

Steps

順次実行 (コードはリンク先に)。

f:id:kondoumh:20210611131108p:plain

- - で先行ステップを表現するのはややわかりづらい気がします。

でも DAG っていう GitHub Actions とかでもお馴染みの記法もサポートされてます。

DAG

f:id:kondoumh:20210611131224p:plain

Artifacts

後続のステップに成果物を受け渡せます。サンプルでは [MinIO] のストレージをマウントしてますが、S3 とか色々サポートされてます。

Scripts & Results

source タグに直接コード書けます。Node.js とか Python のイメージ持ってくれば、ベタっとスクリプト書けて便利そうです。

apiVersion: argoproj.io/v1alpha1
kind: Workflow
metadata:
  generateName: scripts-bash-
spec:
  entrypoint: bash-script-example
  templates:
  - name: bash-script-example
    steps:
    - - name: generate
        template: gen-random-int-bash
    - - name: print
        template: print-message
        arguments:
          parameters:
          - name: message
            value: "{{steps.generate.outputs.result}}"  # The result of the here-script

  - name: gen-random-int-bash
    script:
      image: debian:9.4
      command: [bash]
      source: |                                         # Contents of the here-script
        cat /dev/urandom | od -N2 -An -i | awk -v f=1 -v r=100 '{printf "%i\n", f + r * $1 / 65536}'

  - name: gen-random-int-python
    script:
      image: python:alpine3.6
      command: [python]
      source: |
        import random
        i = random.randint(1, 100)
        print(i)

  - name: gen-random-int-javascript
    script:
      image: node:9.1-alpine
      command: [node]
      source: |
        var rand = Math.floor(Math.random() * 100);
        console.log(rand);

  - name: print-message
     inputs:
      parameters:
      - name: message
    container:
      image: alpine:latest
      command: [sh, -c]
      args: ["echo result was: {{inputs.parameters.message}}"]

Loops

繰り返し。パラメータとかに List や Map を書いて Matrix build 的なこともできます。いろんな環境で並列にテストするとかが簡単です。

apiVersion: argoproj.io/v1alpha1
kind: Workflow
metadata:
  generateName: loops-maps-
spec:
  entrypoint: loop-map-example
  templates:
  - name: loop-map-example
    steps:
    - - name: test-linux
        template: cat-os-release
        arguments:
          parameters:
          - name: image
            value: "{{item.image}}"
          - name: tag
            value: "{{item.tag}}"
        withItems:
        - { image: 'debian', tag: '9.1' }       #item set 1
        - { image: 'debian', tag: '8.9' }       #item set 2
        - { image: 'alpine', tag: '3.6' }       #item set 3
        - { image: 'ubuntu', tag: '17.10' }     #item set 4

  - name: cat-os-release
    inputs:
      parameters:
      - name: image
      - name: tag
    container:
      image: "{{inputs.parameters.image}}:{{inputs.parameters.tag}}"
      command: [cat]
      args: [/etc/os-release]

実行結果。並列で動いてます。

f:id:kondoumh:20210611131255p:plain

Conditional

条件分岐は when タグで書きます。Coin flip のサンプルです。実行されたステップとされなかったステップが識別できます。

f:id:kondoumh:20210611131313p:plain

Retrying Failed or Errord Steps

エラーのリトライも指定できます。特徴的なのは、nodeAntiAffinity の設定で、失敗した時とは別のワーカーノードで実行されることで環境要因のエラーが切り分けられる可能性があります。Kubernetes の特性を活かしてる感があります。

retry-backoff.yaml

 apiVersion: argoproj.io/v1alpha1
 kind: Workflow
 metadata:
   generateName: retry-backoff-
 spec:
   entrypoint: retry-backoff
   templates:
   - name: retry-backoff
     retryStrategy:
       limit: 10
       retryPolicy: "Always"
       backoff:
         duration: "1" # Must be a string. Default unit is seconds. Could also be a Duration, e.g.: "2m", "6h", "1d"
         factor: 2
         maxDuration: "1m" # Must be a string. Default unit is seconds. Could also be a Duration, e.g.: "2m", "6h", "1d"
       affinity:
         nodeAntiAffinity: {}
     container:
       image: python:alpine3.6
       command: ["python", -c]
       # fail with a 66% probability
       args: ["import random; import sys; exit_code = random.choice([0, 1, 1]); sys.exit(exit_code)"]

f:id:kondoumh:20210611131331p:plain

Exit handlers

ワークフローが成功しても失敗しても実行したいステップ(結果を通知するなど) を記述するときは Exit handler を使います。

f:id:kondoumh:20210611131346p:plain

Suspending

suspend タグで停止のステップを入れることが可能です。duration で時間も指定できます。Argo CLI の resume サブコマンドで外部からレジュームすることも可能です。

 $ argo resume WORKFLOW

Daemon Containers

template のスコープで、指定したコンテナを Daemon として起動、破棄できます。そのスコープでは常に使用できるので、コンテナ化した Web アプリのテストをするとか、一時的に Docker レジストリとか NPM のパッケージレジストリを建ててテスト中だけ使うなどの用途が考えられます。

Sidecars

メインのコンテナと同一 Pod で起動するサイドカーコンテナを指定することもできます。

サイドカーを使って DinD な処理も書けます。

Docker-in-Docker Using Sidecars

Kubernetes Resources

Kubernetes のマニフェストを直接 workflowに記述して適用することも可能です。

以上のように Kubernetes ネイティブでかつ CI パイプラインの記述でお馴染みの書き方でワークフローを作れる Argo Workflow なかなかイイですね。色々なコンテナでコードもぱぱっとかけちゃうし。

CI にも使いたいぜって思いますが、今のところ git push で trigger できないです。でも、

but we plan to do so in the near future.

と近い将来対応されるようなので、それまでは cron ジョブとかで起動してねとのことです。

Kubernetes native なワークフローエンジン Argo workflow を動かす

Argo workflow は Cloud Native Computing Foundation にホストされている Kubernetes native なワークフローエンジンです。

github.com

Kubeflow でも機械学習のパイプラインエンジンとして使用されています。

blog.kondoumh.com

公式の manifest でインストールできます。

https://github.com/argoproj/argo-workflows/tree/master/manifests

Helm charts もコミュニティベースでメンテナンスされています。

argo-helm/charts/argo at master · argoproj/argo-helm · GitHub

今回は Docker Desktop の Kubernetes 環境に argo namespace を作成して quick-start 用の manifest を適用してみました。

$ kubectl create namespace argo
$ kubectl apply -n argo -f https://raw.githubusercontent.com/argoproj/argo-workflows/stable/manifests/quick-start-postgres.yaml

argo-server と workflow-controller が起動し、ストレージ用の minio と DB サーバ postgres も起動されました。

$ kubectl get po,deploy,svc -n argo            
NAME                                      READY   STATUS    RESTARTS   AGE
pod/argo-server-5b86d9f84b-vjhhq          1/1     Running   4          23m
pod/minio-58977b4b48-ts9zn                1/1     Running   0          23m
pod/postgres-6b5c55f477-ss24g             1/1     Running   0          23m
pod/workflow-controller-d9cbfcc86-jkf8j   1/1     Running   2          23m

NAME                                  READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/argo-server           1/1     1            1           23m
deployment.apps/minio                 1/1     1            1           23m
deployment.apps/postgres              1/1     1            1           23m
deployment.apps/workflow-controller   1/1     1            1           23m

NAME                                  TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)    AGE
service/argo-server                   ClusterIP   10.106.203.74   <none>        2746/TCP   23m
service/minio                         ClusterIP   10.96.161.227   <none>        9000/TCP   23m
service/postgres                      ClusterIP   10.98.163.40    <none>        5432/TCP   23m
service/workflow-controller-metrics   ClusterIP   10.102.138.81   <none>        9090/TCP   23m

CRD もインストールされました。

$ kubectl get crd | grep argo
NAME                                   CREATED AT
clusterworkflowtemplates.argoproj.io   2021-05-06T03:32:28Z
cronworkflows.argoproj.io              2021-05-06T03:32:28Z
workfloweventbindings.argoproj.io      2021-05-06T03:32:28Z
workflows.argoproj.io                  2021-05-06T03:32:28Z
workflowtemplates.argoproj.io          2021-05-06T03:32:28Z

Argo Server の Web UI を使うために port-forward します。

kubectl -n argo port-forward deployment/argo-server 2746:2746

Chrome で https://localhost:2746 に接続すると NET::ERR_CERT_INVALID と自己証明書のエラーで接続できないので、thisisunsafe の呪文をタイプして接続しました。

f:id:kondoumh:20210507083016p:plain

まだワークフローは何も登録されていない状態です。

Argo workflow は kubectl コマンドでも操作できますが、Argo CLI に便利コマンドが用意されているのでインストールします。

$ curl -sLO https://github.com/argoproj/argo/releases/download/v3.0.2/argo-linux-amd64.gz
$ gunzip argo-linux-amd64.gz
$ chmod +x argo-linux-amd64
$ mv ./argo-linux-amd64 /usr/local/bin/argo

サンプルワークフローをデプロイしてみます。hello-world- というプレフィクス付きのワークフローを生成します。whalesay という template が定義されており、コンテナイメージ whalesay を起動して cowsay コマンドを叩くものです。メモリと CPU リソースの limit も指定しています。この template を entrypoint に指定することで起動します。

apiVersion: argoproj.io/v1alpha1
kind: Workflow
metadata:
  generateName: hello-world-
  labels:
    workflows.argoproj.io/archive-strategy: "false"
spec:
  entrypoint: whalesay
  templates:
  - name: whalesay
    container:
      image: docker/whalesay:latest
      command: [cowsay]
      args: ["hello world"]
      resources:
        limits:
          memory: 32Mi
          cpu: 100m
$ argo submit -n argo --watch https://raw.githubusercontent.com/argoproj/argo-workflows/master/examples/hello-world.yaml

Name:                hello-world-mg6x7
Namespace:           argo
ServiceAccount:      default
Status:              Succeeded
Conditions:          
 PodRunning          False
 Completed           True
Created:             Tue May 11 12:27:50 +0900 (1 minute ago)
Started:             Tue May 11 12:27:50 +0900 (1 minute ago)
Finished:            Tue May 11 12:29:25 +0900 (now)
Duration:            1 minute 35 seconds
Progress:            1/1
ResourcesDuration:   35s*(100Mi memory),35s*(1 cpu)

STEP                  TEMPLATE  PODNAME            DURATION  MESSAGE
 ✔ hello-world-mg6x7  whalesay  hello-world-mg6x7  1m 

1分ほどでコンテナ作成から実行までが完了しました。

最新のジョブの状態やログを Argo CLI で確認できます。

$ argo list -n argo @latest
NAME                STATUS      AGE   DURATION   PRIORITY
hello-world-mg6x7   Succeeded   23m   1m         0

$ argo logs -n argo @latest
hello-world-mg6x7:  _____________ 
hello-world-mg6x7: < hello world >
hello-world-mg6x7:  ------------- 
hello-world-mg6x7:     \
hello-world-mg6x7:      \
hello-world-mg6x7:       \     
hello-world-mg6x7:                     ##        .            
hello-world-mg6x7:               ## ## ##       ==            
hello-world-mg6x7:            ## ## ## ##      ===            
hello-world-mg6x7:        /""""""""""""""""___/ ===        
hello-world-mg6x7:   ~~~ {~~ ~~~~ ~~~ ~~~~ ~~ ~ /  ===- ~~~   
hello-world-mg6x7:        \______ o          __/            
hello-world-mg6x7:         \    \        __/             
hello-world-mg6x7:           \____\______/

もちろん Web UI でも結果を閲覧できます。

f:id:kondoumh:20210511123323p:plain

f:id:kondoumh:20210511123343p:plain

ジョブの詳細画面からログ出力を確認できます。

f:id:kondoumh:20210511124600p:plain

f:id:kondoumh:20210511124623p:plain

タスクの依存関係を dependencies で指定するサンプルです。

# The following workflow executes a diamond workflow
# 
#   A
#  / \
# B   C
#  \ /
#   D
apiVersion: argoproj.io/v1alpha1
kind: Workflow
metadata:
  generateName: dag-diamond-
spec:
  entrypoint: diamond
  templates:
  - name: diamond
    dag:
      tasks:
      - name: A
        template: echo
        arguments:
          parameters: [{name: message, value: A}]
      - name: B
        dependencies: [A]
        template: echo
        arguments:
          parameters: [{name: message, value: B}]
      - name: C
        dependencies: [A]
        template: echo
        arguments:
          parameters: [{name: message, value: C}]
      - name: D
        dependencies: [B, C]
        template: echo
        arguments:
          parameters: [{name: message, value: D}]

  - name: echo
    inputs:
      parameters:
      - name: message
    container:
      image: alpine:3.7
      command: [echo, "{{inputs.parameters.message}}"]

CLI の実行結果

 ✔ dag-diamond-vqswz  diamond                                             
 ├─✔ A                echo      dag-diamond-vqswz-4245884217  16s         
 ├─✔ B                echo      dag-diamond-vqswz-4195551360  6s          
 ├─✔ C                echo      dag-diamond-vqswz-4212328979  6s          
 └─✔ D                echo      dag-diamond-vqswz-1249778     5s 

Web UI での表示

f:id:kondoumh:20210515083827p:plain

以上、ローカルでの Argo 環境構築と動作確認でした。

GitHub CLI に GitHub Actions 用コマンドが追加された

1.0 では対応してませんでした。

blog.kondoumh.com

今月の中旬にリリースされた v1.9.0 で run, workflow, actions のコマンド群が提供されました。

github.blog

actions でコマンド一覧が表示できます。

$ gh actions

f:id:kondoumh:20210429150834p:plain

run はワークフロー実行結果や詳細を見るためのコマンド。workflow はワークフローファイルの管理や起動のめのコマンドのようです。

リポジトリのワークフロー実行結果一覧を取得するには run list サブコマンドを使用します。

$ gh run list

f:id:kondoumh:20210429143053p:plain

ワークフロー起動時のコミット、ワークフロー名、ブランチ、トリガー、run ID が取得されます。

実行中のワークフローをウォッチするには run watch サブコマンドを使用します。

$ gh run watch

f:id:kondoumh:20210429145055p:plain

実行中のジョブを選択できて、選択するとワークフローのジョブごとにステップの実行結果が順次出力されていきます。Web UI だとステップごとの結果はジョブをクリックしないと見れませんので一覧性は高くなっています。

f:id:kondoumh:20210429145247p:plain

run view サブコマンドで run ID を指定して実行結果の詳細を取得できます。

$ gh run view <run ID>

f:id:kondoumh:20210429150136p:plain

Web UI もよくできているのででエラー箇所を見るのは簡単ですが、CLI を使うと grep などと組み合わせて出力を加工できるのでこれはこれで便利です。

この他ワークフローを再実行する run rerun や ワークフロー実行により生成された Artifact をダウンロードする run download などのサブコマンドも提供されています。

ワークフローファイルの一覧は workflow list サブコマンドで取得できます。

$ gh workflow list

f:id:kondoumh:20210429152131p:plain

ワークフロー名を指定して実行できます。

$ gh workflow run <workflow name>

f:id:kondoumh:20210429152523p:plain

実行後は run watch で実行状態を監視できます。

今のところ実行できるのは workflow_dispatch トリガーを持つワークフローだけです。このトリガーを持たないワークフローを実行しようとするとエラーになります。

f:id:kondoumh:20210429153048p:plain

実行後は run watch で実行状態を監視できます。

ブラウザでポチポチしなくてもワークフローの状態を調べることができるようになって便利になりました。

ワークフロー一覧はもう少し属性情報を出して欲しいですね。workflow view サブコマンドで詳細が出ますが、YAML の解析が大変なのかも? あとworkflow_dispatch だけでなく webhook イベントでトリガーするrepository_dispatch の実行にも対応して欲しいところです。

GitHub リリースページのドラフトを自動生成してくれる Release Drafter を導入する

リリースページに ChangeLog というかリリースノートを書くのはかなり面倒です。 Release Drafter を使うと PR 単位でいい感じに リリースページのドラフトを生成してくれます。

Release Drafter の GitHub App は簡単に導入できますが、何やら Deprecated にしようという議論があるようです。

GitHub Apps - Release Drafter · GitHub

github.com

GitHub Actions で release-drafter 用のワークフローを使う方が無難かなと思います。

github.com

README にある通り、設定用の release-drafter.yml とワークフロー用の release-drafter.yml をそれぞれリポジトリの .github.github/workflows に配置するだけ。

設定用の release-drafter.yml。PR にラベルをつけておくと、Features や Bug Fixes に振り分けてくれます。ChangeLog に入れたくないような修正については除外用のラベルを指定することもできます。

name-template: 'v$NEXT_PATCH_VERSION'
tag-template: 'v$NEXT_PATCH_VERSION'
categories:
  - title: '🚀 Features'
    labels:
      - 'feature'
      - 'enhancement'
  - title: '🐛 Bug Fixes'
    labels:
      - 'fix'
      - 'bugfix'
      - 'bug'
  - title: '🧰 Maintenance'
    labels:
      - 'chore'
change-title-escapes: '\<*_&'
exclude-labels:
  - 'exclude from changelog'
template: |
   ## Changes

   $CHANGES

ワークフロー用の release-drafter.yml です。

name: Release Drafter

on:
  push:
    branches:
      - master
  pull_request:
    types: [opened, reopened, synchronize]

jobs:
  update_release_draft:
    runs-on: ubuntu-latest
    steps:
      - uses: release-drafter/release-drafter@v5
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

PR が作成・マージされる度に Draft が更新されていきます。

f:id:kondoumh:20210326150153p:plain

template に 'v$NEXT_PATCH_VERSION' を指定しているので、0.1.0 がリリースされている状態だと 0.1.1 のドラフトができます。

f:id:kondoumh:20210326150331p:plain

Draft を Edit して実際に作成したリリースページ。

f:id:kondoumh:20210326150353p:plain

手作業でリリース時にあたふたやってたことが継続的にオートメーション化された感じですね。PR のタイトルをちゃんと付けるようにすれば、ほとんど修正は不要でしょう。