もうsedのコマンドの検索はしないぞ github.com
プルリクエストが承認+テストをパスしたら通知するmacOSアプリを作った
プルリクエストがマージ可能になったらいち早く知りたいので、承認+テストをパスしたら通知を送るmacOSのアプリを作った。
approve不要なPRについてはテストが完了した時点で通知がくる(はず)。 テストがこけても通知が来る。rejectされても通知が来る。要はステータスが確定したら通知が来る。
Notificationsをみてもいいんだけど、approveやテスト以外の通知もくるので専用に作ってみた。 Notifications全般の通知を受け取りたいならGitifyを使った方がいいと思う。
通知はこんな感じ。
メニューバーのアイコンをクリックするとステータスが確定したPRの一覧が出る。
あとステータスが確定したPRがないときは、メニューバーのアイコンが黒に変わる。
設定画面で検索条件を変えられるので、特定のorgだけにするとか、一部orgを除外するとかできる。
実装
以下のようなGraphQLを実行して、PR一覧とテストのステータスをまとめて取得している。
https://github.com/winebarrel/Succ/blob/main/Succ/Github/SearchPullRequests.graphql
query SearchPullRequests($query: String!) { search(type: ISSUE, last: 100, query: $query) { nodes { ... on PullRequest { repository { name owner { login } } title url reviewDecision commits(last: 1) { nodes { commit { url statusCheckRollup { state } } } } } } } }
取得したPRのステータスを見て通知するかどうかを判断。
https://github.com/winebarrel/Succ/blob/main/Succ/PullRequest.swift
private func updateNodes(_ value: GraphQLResult<Github.SearchPullRequestsQuery.Data>) { var fetchedNodes: Nodes = [] value.data?.search.nodes?.forEach { body in if let pull = body?.asPullRequest { let reviewDecision = pull.reviewDecision if reviewDecision != nil && reviewDecision != .approved && reviewDecision != .changesRequested { return } guard let commit = pull.commits.nodes?.first??.commit else { return } guard let state = commit.statusCheckRollup?.state else { return } if state != .success && state != .failure && state != .error { return } let node = Node( owner: pull.repository.owner.login, repo: pull.repository.name, title: pull.title, url: pull.url, reviewDecision: reviewDecision?.rawValue ?? "", state: state.rawValue, commitUrl: commit.url, success: (reviewDecision == nil || reviewDecision == .approved) && state == .success ) fetchedNodes.append(node) } }
Golangの構造体の情報をダンプするライブラリを作った
Golangの構造体の情報をダンプするライブラリを作った。
使い方
こういう感じの設定用structがあったとして
type config struct { Home string `env:"HOME,required"` Port int `env:"PORT" envDefault:"3000"` Bar *subconfig `envPrefix:"SUB_"` } type subconfig struct { Password string `env:"PASSWORD,unset,required"` IsProduction bool `env:"PRODUCTION"` }
ダンプしてJSONで出力できる。
func main() { var c config ss := tipper.Dump(c) //ss := tipper.DumpT[config]() fmt.Println(ss[0].Fields[0]) //=> "{Password string [{env PASSWORD [unset required]}]}" fmt.Println(ss) }
[ { "name": "main.subconfig", "fields": [ { "name": "Password", "type": "string", "tags": [ { "key": "env", "name": "PASSWORD", "options": [ "unset", "required" ] } ] }, // ...
ユースケース
「Golangのプログラムでconfig構造体にフィールドを追加したが、ECSタスク定義に環境変数を追加していなかったためデプロイしたらCIがこけた」ということがたまにあるので、プログラム自身に必要な環境変数を出力させて、テストでECSタスク定義の環境変数と比較する…というようなユースケースを考えている。
上記の例だと、必須な環境変数の一覧をjqでとれる。
$ go run main.go | jq -r '.[].fields[] | select(.tags).tags[] | select(.key == "env" and (.options // [] | contains(["required"]))).name' PASSWORD HOME
おまけ: Struct Tagのパース
このライブラリではStruct Tagのパースに https://github.com/fatih/structtag を使っている。 キーから値を取得するだけなら、refrect.StructTag.Get()でできるが、すべてのキーを取得するようなことはできないので。
同様のライブラリはいくつかあるが、アクティブに更新されているものは見つけられなかった。
- https://github.com/moznion/go-struct-custom-tag-parser
- https://github.com/execjosh/structtag
- https://github.com/go-mods/tags
実装自体がシンプルなためあまり問題になることはないと思うが、できれば標準ライブラリでサポートしてほしい…
GitHubの「Finish your review」から「Comment」を消すChrome拡張を作った
https://chromewebstore.google.com/detail/github-hide-finish-commen/ejflccgjhcloeienodjdmngdhockjbdf
10回に1回ぐらい「Approveしたと思ったらCommentだった」ということがあるので、Finish your review
からComment
を消す拡張を作ってみた。
シングルバイナリで動くERBのテンプレートプロセッサーを作った
本体は ERB.new().result
を呼ぶだけで、それをmrubyでdarwin/linuxのx86_64/aarch64向けにビルドした。
以下のようにシングルバイナリプログラムを通してテンプレートファイルを処理できる。
<%- to = ENV["MAIL_TO"] priorities = ENV["PRIORITIES"].split(",").map(&:strip) -%> From: James <james@example.com> To: <%= to %> Subject: Addressing Needs <%= to[/\w+/] %>: Just wanted to send a quick note assuring that your needs are being addressed. I want you to know that my team will keep working on the issues, especially: <%# ignore numerous minor requests -- focus on priorities %> <%- priorities.each do |priority| -%> * <%= priority %> <%- end -%> Thanks for your patience. James
$ export MAIL_TO="Community Spokesman <spokesman@example.com>" $ export PRIORITIES="Run Ruby Quiz,Document Modules,Answer Questions on Ruby Talk" $ minierb mail.erb From: James <james@example.com> To: Community Spokesman <spokesman@example.com> Subject: Addressing Needs Community: Just wanted to send a quick note assuring that your needs are being addressed. I want you to know that my team will keep working on the issues, especially: * Run Ruby Quiz * Document Modules * Answer Questions on Ruby Talk Thanks for your patience. James
Dockerコンテナを動かすときに設定値の注入のため環境変数経由で設定ファイルを書き換えることがよくある。 sedで頑張っているDockerイメージもあるし、Docker社の提供するnginxイメージだと組み込みでenvsubstがついてくる。
しかし、単純にプレースホルダを環境変数で置換するだけだと、全然機能が足りないと思っていて、条件分岐や繰り返し、デフォルト値の設定が欲しくなる。 Rubyには組み込みでERBが含まれているが、nginxなどのコンテナにRubyランタイム一式をインストールはしたくない。 そうすると選択肢が限られてきて、自分はGoのテンプレートが使えるsigilを使うことが多かった。
Goのテンプレートは個人的には結構好きだが、パイプラインのような書き方を好まない人もいそうだなとか、もう少しだけテキスト処理を便利にしてほしいなどのニーズがありそうだな…ということで、minierbを作ってみた。
他のエンジニアがDocker向けにどのようなテンプレートプロセッサーを利用しているのか、とても気になる。
参考
lambdazip providerでnode_moduleを含むzipをLambdaにデプロイする
terraformでaws_lambda_functionとarchive_fileを使ってLambdaをデプロイする方法がある。
data "archive_file" "lambda" { type = "zip" source_file = "lambda.js" output_path = "lambda_function_payload.zip" } resource "aws_lambda_function" "test_lambda" { filename = data.archive_file.data.archive_file.nyan.output_path.output_path function_name = "lambda_function_name" role = aws_iam_role.iam_for_lambda.arn handler = "index.handler" source_code_hash = data.archive_file.lambda.output_base64sha256 runtime = "nodejs18.x" }
しかし、node_modulesと相性が悪くて、npm i
の実行タイミングが難しかったり、terraformのCIで毎回差分が出たりする。
リソースの方のarchive_fileとnull_resourceを組み合わせれば出来るかもしれないが、archive_file (Resource)はすでに非推奨で、やり方を考えるのにも手間がかかる。
なのでzipファイルのハッシュ値はtfstateに保持しつつ、ソースコードやpackage.jsonが変わったらnpm i
とzipファイルの再作成を行うterraform providerを作った。
使い方
以下のようなファイル構成だったとして
./ |-- lambda/ | |-- index.js | |-- node_modules/ | |-- package-lock.json | `-- package.json `-- main.tf
main.tfは次のようになる。
terraform { required_providers { lambdazip = { source = "winebarrel/lambdazip" version = ">= 0.5.0" } } } data "lambdazip_files_sha256" "triggers" { files = ["lambda/*.js", "lambda/*.json"] } resource "lambdazip_file" "app" { base_dir = "lambda" sources = ["**"] excludes = [".env"] output = "lambda.zip" before_create = "npm i" triggers = data.lambdazip_files_sha256.triggers.map } resource "aws_lambda_function" "app" { filename = lambdazip_file.app.output function_name = "my_func" role = aws_iam_role.lambda_app_role.arn handler = "index.handler" source_code_hash = lambdazip_file.app.base64sha256 runtime = "nodejs20.x" } resource "aws_iam_role" "lambda_app_role" { name = "lambda-app-role" assume_role_policy = jsonencode({ Version = "2012-10-17" Statement = [ { Effect = "Allow" Principal = { Service = "lambda.amazonaws.com" } Action = "sts:AssumeRole" } ] }) } resource "aws_iam_role_policy_attachment" "lambda_app_role" { role = aws_iam_role.lambda_app_role.name policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" }
- lambdazip_file.app作成時には
npm i
が実行されてからzipファイルが作成される - その後はtriggers属性に含まれるファイルを変更した場合にzipファイルが再作成されLamadaがデプロイされる
- node_modulesやlambda.zipを削除しても、base64sha256属性は変わらないのデプロイはされない
- CIでterraformを実行する場合も、node_modulesの有無にかかわらず不必要な差分がでることはない
おまけ: Goのデプロイ
GoをLambdaにデプロイする場合は、archive_fileとnull_resourceだけで出来るかもしれないが、lambdazipを使うとよりシンプルにかけると思う。
./ |-- lambda/ | |-- go.mod | |-- go.sum | `-- main.go `-- main.tf
data "lambdazip_files_sha256" "triggers" { files = ["lambda/*.go", "lambda/go.mod", "lambda/go.sum"] } resource "lambdazip_file" "app" { base_dir = "lambda" sources = ["bootstrap"] output = "lambda.zip" before_create = "GOOS=linux GOARCH=amd64 go build -o bootstrap main.go" triggers = data.lambdazip_files_sha256.triggers.map } resource "aws_lambda_function" "app" { filename = lambdazip_file.app.output function_name = "my_func" role = aws_iam_role.lambda_app_role.arn handler = "my-handler" source_code_hash = lambdazip_file.app.base64sha256 runtime = "provided.al2023" }
Azure Entra IDのエンタープライズアプリケーションを取得する
cf. アプリケーションを取得する - Microsoft Graph v1.0 | Microsoft Learn
#!/usr/bin/env python # pip install azure-identity msgraph-sdk import asyncio import pprint from azure.identity.aio import ClientSecretCredential from msgraph import GraphServiceClient client_id = "..." tenant_id = "..." client_secret = "..." credentials = ClientSecretCredential( tenant_id=tenant_id, client_id=client_id, client_secret=client_secret ) scopes = ["https://graph.microsoft.com/.default"] client = GraphServiceClient(credentials=credentials, scopes=scopes) async def print_app(): apps = await client.applications.get() for app in apps.value: pprint.pprint(app.display_name) asyncio.run(print_app())