- 投稿日:
Groovy Scriptの読み方が分からなかったので読み解き方のメモ。全て憶測
確認環境
Env | Ver |
---|---|
Jenkins | 2.249.1 |
Groovy Script | 不明 |
サンプルコード
def credentials = com.cloudbees.plugins.credentials.CredentialsProvider.lookupCredentials(
org.jenkinsci.plugins.plaincredentials.StringCredentials.class,
jenkins.model.Jenkins.instance.getItemByFullName("ここにフォルダパス")
)
def cred = credentials.findResult { it.id == "ここに取得したいやつのCredentials ID" ? it : null }
読み解き方
個人的な解釈なので特に根拠はない。全て憶測。
lookupCredentialsの部分
端的に言うとデータ型とストア名のようなものを指定し、認証情報を取得する機能であると思われる。Groovy Script的にはCollectionが返ってくる。
- com.cloudbees.plugins.credentials.CredentialsProvider.lookupCredentials())
- 第一引数にデータ型、第二引数にLOOKUP条件を設定するものと思われる
- org.jenkinsci.plugins.plaincredentials.StringCredentials.class
- 文字列の認証情報ということだと思われる
- jenkins.model.Jenkins.instance.getItemByFullName())
- 現在のJenkinsインスタンス(Jenkinsシステムそのもの)から名前で要素を取得するものだと思われる
findResultの部分
- http://docs.groovy-lang.org/docs/groovy-2.1.3/html/groovy-jdk/java/util/Collection.html#findResult(groovy.lang.Closure)
- 恐らくJSのArray.findと似たような機能で、
it
の中に配列要素が入ってくるので、それを使って配列内容の要素を取り出すのに使うのだと思う
- 恐らくJSのArray.findと似たような機能で、
- 投稿日:
HuskyとはNode.jsを利用した開発で非常によく使われているGit hooksのユーティリティだが、個人的にはこのツールの存在価値に疑問を感じている。
という訳で、この記事ではHuskyのメリデメを考えた結果と、Huskyが何をしているか、Huskyの必要性について書いてゆく。
Huskyがあるメリット
Huskyは極めて有名なツールであるため、Huskyが入っているとこのプロジェクトはGit hooksで管理されており、標準化されていることが確認できるだろう。恐らくHuskyのメリットはこれ以外に存在しないと考えている。
Huskyがあるデメリット
Huskyの管理をしないといけない、Huskyも地味にアップデートするからだ。これは明確なコストである。
そしてHuskyのコードやリポジトリを見たことがある人は多分ほとんどいないと思う。更に言えばHuskyが何をしているかすらも知らない人だっているはずだ。そんな得体の知れないものを使うのは怖いというところだ。
Huskyは何をしているか?
端的に言えばGit hooksのパスを .husky/
に設定しているだけである。
要するに git config core.hooksPath .husky
を叩いているだけだ。
もう少し細かく言えば以下に相当する処理を実行している。
mkdir -p .husky/_
cp husky.sh .husky/_
git config core.hooksPath .husky
勿論、ソースコードには他の処理も書かれているのだが、実質的には上記三行が全てと言って良い。
husky.sh
を活用しているケースがどれほどあるか怪しいことを考えると、本質は git config core.hooksPath .husky
だと思うので、正直あるだけ邪魔では?と考えている。
Huskyの必要性
ここまででHuskyがしていることは git config core.hooksPath .husky
だということが解ったが、だとしたらHuskyは本当に必要なのだろうか?私は特に理由がないのであれば package.json
で husky install
と書いてあるところに git config core.hooksPath .githooks
とでも書いておけば良いのではないか?と思っている。恐らく何も不都合はないはずだ。
ただ世の中には色々な事情があり、使わざるを得ないケースもあると思う。しかし、可能であれば排除してもいいのではないか?個人的にはそう思っている。
何故この記事を書いたか
「この世からHuskyを滅ぼすため」というのはまぁ冗談だが、個人的にHuskyの存在価値があまり良くわかっておらず、多分世間の人もあまり理解できていないと勝手に考えていて、可能であればプロジェクトに入れたくないと考えているので、そのお気持ち表明というか、そんな感じだ。
ここからは余談だが、Huskyには結構な数のスポンサーが付いていて、恐らく毎月それなりの収入があると思われる。以下はHuskyのスポンサーである。
個人的にHuskyは最も成功したOSSの一つではないかと考えている。理由としてHusky自体は非常に単純なプロダクトであり、コミット履歴を見てもさしたるメンテナンスがされておらず、ほぼ手放しで維持されていると思われるからだ。
しかし、Huskyはそれなりの額の寄付を集めており、この記事を書いた時点で確認できるだけでも最低 10USD * (4 + 16) + 100USD * (4 + 2)
の寄付がされており、つまり800USD、日本円にして11.2万円ほどだ。何もしてないのに毎月この収入があるのは大分ありがたいだろう。他のOSSならIssueやPull Requestsに対して対応したり、コード本体のメンテナンスがあるはずだが、Huskyにそんなものはないため、プロダクトの維持コストに対して非常によく寄付を集められていると感じる。
- 投稿日:
runas
では上手く出来ないっぽいのでPowerShellを呼んで実行する。管理者実行したいコマンドを分けたい時に活用できる。
確認環境
Env | Ver |
---|---|
Windows 11 Pro | 22621.1413 |
サンプルコード
powershell start-process <ここにコマンド> -verb runas
備考
PowerShellのリファレンスに沿って書くなら大文字小文字を書き分ける方がより正しいが、基本的に大文字と小文字を区別しないため、どっちでも動く。
powershell Start-Process <ここにコマンド> -Verb runAs
- 投稿日:
この記事ではGitHub ActionsのCustom actionをJavaScriptで実装するJavaScript actionをTypeScriptとSWCを使って実装した方法を書いてます。
モチベーション
- GitHub ActionsのWorkflowsを共通化したい
- TypeScriptでロジックを書きたい
- SWCを使いたい(nccはtscを使うので避けたい
- github-scriptは一定以上のボリュームがあるものには向かない
- これを使いつつUTを書いたりするとなると結構面倒になると思う
今回作るもの
Custom actionのうちJavaScript actionを作成します。
実装コードはTypeScript、トランスパイラはSWC、バンドラはwebpackを利用します。
バンドラを利用するのは、node_modules/
をGitで管理したくないためです。
ビルド成果物であるdist/
は実行時に必要なため、Gitで管理します。
(CI上でビルドしてキャッシュさせておくことも出来ると思いますが、今回は扱いません)
確認環境
Env | Ver |
---|---|
@actions/core | 1.10.0 |
@actions/github | 5.1.1 |
@swc/cli | 0.1.57 |
@swc/core | 1.3.26 |
swc-loader | 0.2.3 |
typescript | 4.9.5 |
webpack | 5.75.0 |
webpack-cli | 5.0.1 |
サンプルコード
Custom Actions本体
Custom action本体のサンプルコードです。以下に一式があります。
https://github.com/Lycolia/typescript-code-examples/tree/main/swc-ts-custom-actions
ディレクトリ構成
dist/
配下を叩くため、ここはGit管理に含めます。バンドルするのでnode_modules/
はGit管理から外して問題ありません。
├─dist/
│ └─index.js # Custom Actionsとして実行するファイル本体
├─node_modules/
├─src/
│ └─index.ts # TypeScript実装
├─action.yaml # Custom Actionsの定義
├─package-lock.json
├─package.json
├─swcrc-base.js # SWCの設定
├─tsconfig.json # tscの設定
└─webpack.config.js # webpackの設定
swcrc-base.js
SWCの設定例。特にJavaScript actionのための設定はなく、CLI向けのトランスパイルが出来る設定ならおk。
ファイル名は何でも大丈夫ですが、この場では.swcrc
にしないことで、直接SWCで利用しないことを判りやすくするために違う名前にしています。
module.exports = {
module: {
type: 'commonjs',
},
jsc: {
target: 'es2020',
parser: {
syntax: 'typescript',
tsx: false,
decorators: false,
dynamicImport: false,
},
baseUrl: '.',
paths: {
'src/*': ['src/*'],
},
},
};
webpack.config.js
SWCを使って.ts
ファイルをバンドルするための設定。これがないとimport
の解決ができずにコケます。
node_modules/
配下をGit管理に含める場合は不要かもしれませんが、それをするのは微妙だと思います。
const path = require('path');
const swcrcBase = require(path.resolve(__dirname, 'swcrc-base'));
module.exports = {
// エントリポイント
entry: path.resolve(__dirname, 'src/index.ts'),
// 出力設定
output: {
// クリーンアップ後に出力
clean: true,
// 出力ファイル名
filename: 'index.js',
// 出力パス
path: path.resolve(__dirname, 'dist'),
},
// 設定必須なので何か指定しておく
mode: 'production',
// 指定してないとNode.jsのネイティブAPIが呼べない
target: ['node'],
module: {
// swc-loaderの設定
rules: [
{
test: /\.ts$/,
exclude: /(node_modules)/,
use: {
loader: 'swc-loader',
// swcrcの設定
options: {
...swcrcBase,
},
},
},
],
},
resolve: {
// import時のファイル拡張子を省略してる場合にパスを解決するための設定
extensions: ['', '.ts', '.js'],
},
};
src/index.ts
最低限これだけ確認できれば応用して実装できるだろうという程度のサンプルコード。
@actions/*
系の使い方は以下のリンクから確認できます。
actions/toolkit: The GitHub ToolKit for developing GitHub Actions.
import * as core from '@actions/core';
import * as github from '@actions/github';
const githubToken = core.getInput('GITHUB_TOKEN', { required: true });
const octokit = github.getOctokit(githubToken);
console.log('octokit', octokit);
console.log('context', github.context);
core.setOutput('RESULT_MESSAGE', 'test result message');
action.yaml
実装の参考例として引数と出力を定義してます。特に不要な場合は書かなくてもいいです。
Node.jsのバージョンを詳細に指定したい場合は、composite action にすれば可能だとは思いますが、試してない。
composite actionにしてnvmか何かでインストールしてやれば恐らく可能。
構文は以下のページで確認できます。
GitHub Actions のメタデータ構文 - GitHub Docs
name: example
description: custom actions example
inputs:
GITHUB_TOKEN:
description: 'Repogitory GITHUB_TOKEN'
required: true
outputs:
RESULT_MESSAGE:
description: 'Result message'
on:
workflow_call:
runs:
using: node16
main: dist/index.js
Custom actionを使う側
Custom actionを使うWorkflowのサンプルコードです。以下にソースがあります。
https://github.com/Lycolia/custom-actions-usage-example
.github/workflows/example.yaml
uses
のところにはリポジトリの組織名と、リポジトリ名、action.yaml
が配置されているディレクトリまでのパスを書きます。ルートディレクトリにある場合はパスを書かなくてOK
最後に@sha-hash
でコミットハッシュかタグを付けてやれば呼べるようになります。
動作確認中はハッシュが頻繁に変わるので、最新のハッシュを取得してきて設定されるようにしておくと便利かもしれません。
name: run example
on:
workflow_dispatch:
jobs:
example:
runs-on: ubuntu-latest
steps:
- name: run custom actions
id: test
uses: org-name/repo-name/path/to/file@sha-hash
with:
# Custom action側で定義されている引数(input)の設定
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: show custom actions output
# Custom action側で定義されている出力(output)の取得
run: echo ${{ steps.test.outputs.RESULT_MESSAGE }}
参考資料
- Getting Started – SWC
- swcrcの書き方やswc-loaderの使い方
- Concepts | webpack
- webpackの設定
- カスタム アクションについて - GitHub Docs
- GitHub Actions のメタデータ構文 - GitHub Docs
- プライベート リポジトリからのアクションとワークフローの共有 - GitHub Docs
- actions/toolkit: The GitHub ToolKit for developing GitHub Actions.
@actions/*
系のドキュメントなど
- 投稿日:
割とハマってだるいので今回はサンプル程度にPRの一覧を取得して個別にSlackに投げるものを作ってみます
PRが2個ある場合、出力イメージはこんな感じ。2投稿に分けて投稿します
API トークンの入手
まずはSlack APIを叩くためのトークンをゲットします
- Create an appからアプリを作成
- 左のメニューからFeatures -> OAuth & Permissions
- Scopesを設定
- 今回はBot Token Scopesを
chat:write
とします
- 今回はBot Token Scopesを
- 左のメニューからSettings -> Install App to Your Teamでアプリをインストール
- トークンが吐き出されるのでメモする
GitHub Actions Workflowsの作成
SECRETの設定
- Slack APIトークンをリポジトリのSecretsに突っ込んでおきます
- 名前は一旦
SLACK_TOKEN
とします
Workflowsの作成
前提
- PR一覧の取得には actions/github-script を利用します
- GitHub内部の情報を抜いたり、JSで処理を組みたいときに重宝します
- APIリファレンスが読みやすいので、使うのにはあんま苦労しないと思います
- Slack APIを叩くのにはcurlを利用します
- actions/github-scriptから叩くのは多分難しいです
ベースの作成
これに肉付けをしていきます
name: Post to slack example
on:
workflow_dispatch:
jobs:
post-slack:
runs-on: ubuntu-latest
steps:
PR一覧の取得
List pull requestsにある通りに進めていきます
- uses: actions/github-script@v6
id: set-result
with:
result-encoding: string
script: |
const { data: respPulls } = await github.rest.pulls.list({
owner: context.repo.owner,
repo: context.repo.repo,
});
console.log(respPulls);
PR一覧の加工
こんなデータを取る感じで組んでいきます
API仕様は List pull requests を参照
type PullRequest = {
id: number;
reviewers: string[];
};
先ほど取得したrespPulls
を上記の型付けになるように加工します
const getReviewersName = (requested_reviewers) => {
return requested_reviewers.map((reviewers) => {
return reviewers.login;
});
};
const getPullRequests = (pulls) => {
return pulls.map((pull) => {
return {
id: pull.number,
reviewers: getReviewersName(pull.requested_reviewers),
};
});
};
const pulls = getPullRequests(respPulls);
Slackに投げるメッセージの作成
こんなメッセージをPRの数分組んでいきます
なお実際にSlackでメンションを作る場合はGitHubのスクリーン名とSlackのユーザー IDの突き合わせ処理が別途必要です。やり方は別途後述します
@foo @bar
https://example.com/pulls/1
やっていること
- 上記のフォーマットでメッセージを作成
- シェルスクリプトで配列として扱うためにBase64にエンコード
- 改行コードが混ざっていると扱いづらいので
- エンコードした文字列をスペース区切り文字列として連結
AAA BBB CCC ...
みたいな
- 最後にWorkflowsの戻り値として設定しています
const encodedMessages = pulls.reduce((messages, pull) => {
const reviewersBuff = pull.reviewers
.reduce((acc, cur) => {
return `${acc}${cur} `;
}, '')
.replace(/ $/, '');
const reviewers = reviewersBuff === '' ? 'レビュアー未設定' : reviewersBuff;
const message = `${reviewers}\\nhttps://example.com/pulls/${pull.id}`;
const encodedMessage = Buffer.from(message).toString('Base64');
return `${messages}${encodedMessage} `;
}, '');
return encodedMessages;
curlを利用してSlack APIを叩く
やっていること
encodedMessages=(${{steps.set-result.outputs.result}})
- 前項で作った文字列を配列として取得しています
for message in ${encodedMessages[@]}
- foreach的なやつです
- 改行コードがこの時点で存在すると上手くいきません
decoded_mes=$(echo ${message} | base64 -di)
- ここでBase64エンコードをデコードします
postSlack "$decoded_mes"
- 別引数にならないように
""
で固めます
- 別引数にならないように
- curl叩いてるところ
-d
の中をヒアドキュメントで展開するのが味噌です- 単純に文字列として扱うと変数展開が起きてJSONが壊れます
- run: |
postSlack() {
local mes=$1
curl -sS https://slack.com/api/chat.postMessage \
-H 'Authorization: Bearer ${{ secrets.SLACK_TOKEN }}' \
-H 'Content-Type: application/json; charset=UTF-8' \
-d @- <<EOF
{
token: "${{ secrets.SLACK_TOKEN }}",
channel: "#api-test",
text: "$mes"
}
EOF
}
encodedMessages=(${{steps.set-result.outputs.result}})
for message in ${encodedMessages[@]}
do
decoded_mes=$(echo ${message} | base64 -di)
postSlack "$decoded_mes"
done
コード全体
name: Post to slack example
on:
workflow_dispatch:
jobs:
post-slack:
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v6
id: set-result
with:
result-encoding: string
script: |
const getReviewersName = (requested_reviewers) => {
return requested_reviewers.map((reviewers) => {
return reviewers.login;
});
};
const getPullRequests = (pulls) => {
return pulls.map((pull) => {
return {
id: pull.number,
reviewers: getReviewersName(pull.requested_reviewers),
}
});
}
const { data: respPulls } = await github.rest.pulls.list({
owner: context.repo.owner,
repo: context.repo.repo,
});
const pulls = getPullRequests(respPulls);
const encodedMessages = pulls.reduce((messages, pull) => {
const reviewersBuff = pull.reviewers.reduce((acc, cur) => {
return `${acc}${cur} `
}, '').replace(/ $/, '');
const reviewers = reviewersBuff === '' ? 'レビュアー未設定' : reviewersBuff;
const message = `${reviewers}\\nhttps://example.com/pulls/${pull.id}`;
const encodedMessage = Buffer.from(message).toString('Base64');
return `${messages}${encodedMessage} `
}, '');
return encodedMessages;
- run: |
postSlack() {
local mes=$1
curl -sS https://slack.com/api/chat.postMessage \
-H 'Authorization: Bearer ${{ secrets.SLACK_TOKEN }}' \
-H 'Content-Type: application/json; charset=UTF-8' \
-d @- <<EOF
{
token: "${{ secrets.SLACK_TOKEN }}",
channel: "#api-test",
text: "$mes"
}
EOF
}
encodedMessages=(${{steps.set-result.outputs.result}})
for message in ${encodedMessages[@]}
do
decoded_mes=$(echo ${message} | base64 -di)
postSlack "$decoded_mes"
done
Appendix:Slackにメンションを投げる方法
Slackへ実際にメンションを投げるのはユーザーIDを指定する必要があります
参考:Formatting text for app surfaces
"text": "<@U024BE7LH> Hello"
のようにすることでメンションを投げられます
ユーザーIDはSlackアプリから相手のプロフィールを開き、そこにあるハンバーガーメニューみたいなやつから取れます。一応取得用のAPIもあります