使用Go语言执行GitHub GraphQL API(通过GitHub App认证)
首先
您好,我是株式会社Kakukaku的工程师mamo。
这次在公司内部需要从Go语言调用GitHub GraphQL API,在参考GitHub官方页面等的基础上,尝试实现了一下,现在来给大家介绍一下实现的例子。
作者在调用GitHub API和初次体验golang方面可能还有些欠缺之处,请多多包涵。
前提 – 条件 – (literal translation)
假设 –
只需要一种选择 – Zhǐ
必须事先创建并安装了 GitHub 应用,并获取了 AppID,同时已经下载了密钥。
目标读者
针对想要在没有方便的库的情况下实现GitHub API认证的人。
我认为,如果使用像GitHub上已经公开的现有库,认证过程的实现就可以省去,但是我尝试了根据GitHub官方页面的内容进行实现,这是我尝试的结果。
此外,由于我还是第一次接触golang,所以我在结尾处写下了我的感想。
GitHub 的 GraphQL API?
在GitHub的官方页面上有一个名为「创建GraphQL调用」的页面,我将以此为基础来进行进一步的学习。
首先,GraphQL API有三种认证方法,包括Personal Access Token(PAT)、GitHub App和OAuth App。但是,考虑到本次需求是创建一个能够自动巡回公司内部仓库的爬虫应用,我们选择了GitHub App的认证方法。
由于PAT认证的问题,一个个人用户的权限关联会导致管理上的问题,并且如果用户帐户关闭,也有可能导致爬虫应用程序停止,因此我们在中途放弃了。
GraphQL的通信
如果使用curl命令,语法如下:
curl -H "Authorization: bearer TOKEN" -X POST -d " \
{ \
\"query\": \"query { viewer { login }}\" \
} \
" https://api.github.com/graphql
在这里指定的TOKEN不是JSON Web Token(JWT),而是指定“GitHub App安装认证”页面中提供的安装访问令牌。
安裝存取權杖
创建安装访问令牌需要一些步骤,并且稍微有点麻烦。
-
- 创建JWT。
-
- 使用创建的JWT令牌发送GET请求到/app/installations等,以获取安装ID。
- 使用获取的安装ID(INSTALLATION_ID)发送POST请求到/app/installations/INSTALLATION_ID/access_tokens,以获取安装访问令牌。
在1小时内,您可以使用获取的安装访问令牌来调用GitHub的GraphQL API和REST API。
如果自己实现认证处理,就需要管理这种令牌的有效时间,当时间到期时,需要发行新的令牌并使用,这就需要进行生命周期管理。
本文实例中没有进行生命周期管理。
使用go语言进行实现的示例
GitHubAppID是指定预先创建的GitHub App的AppID。
GitHubAppPrivateKeyFile是指定GitHub App的私钥pem文件。
当GraphQL的执行结果为单一(非数组)或多个(数组)时,处理方式是临时应对的,因此对于此实现是否适当仍存有疑问。
package main
import (
"encoding/json"
"io"
"net/http"
"os"
"strings"
"time"
"github.com/dgrijalva/jwt-go"
)
const (
GitHubAppID = "XXXXXX"
GitHubAppPrivateKeyFile = "commitcrawler.2023-10-30.private-key.pem"
)
/* 汎用関数定義 */
// HTTPリクエストの実行
// (戻り値)
// JSONバイトデータ
func doHttp(req *http.Request) []byte {
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
jsonByte, _ := io.ReadAll(resp.Body)
return jsonByte
}
// JSONバイトデータの変換(入力が単一)
func convJsonByteToMap(jsonByte []byte) map[string]interface{} {
// マップへ変換
var data map[string]interface{}
err := json.Unmarshal([]byte(jsonByte), &data)
if err != nil {
panic(err)
}
return data
}
// JSONバイトデータの変換(入力が複数)
func convJsonBytesToMaps(jsonByte []byte) []map[string]interface{} {
str := string(jsonByte)
parts := strings.Split(str, "\n")
var allData []map[string]interface{}
for _, part := range parts {
part = strings.TrimSpace(part)
if part == "" {
continue
}
var data []map[string]interface{}
err := json.Unmarshal([]byte(part), &data)
if err != nil {
panic(err)
}
allData = append(allData, data...)
}
return allData
}
/* GraphQL問い合わせ用 */
// JWTトークンの取得
// https://docs.github.com/ja/apps/creating-github-apps/authenticating-with-a-github-app/generating-a-json-web-token-jwt-for-a-github-app
func generateJWT() (string, error) {
privateKey, err := os.ReadFile(GitHubAppPrivateKeyFile)
if err != nil {
return "", err
}
key, err := jwt.ParseRSAPrivateKeyFromPEM(privateKey)
if err != nil {
return "", err
}
// Create a new token object
token := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims{
"iat": time.Now().Unix(),
"exp": time.Now().Add(10 * time.Minute).Unix(),
"iss": GitHubAppID,
})
// Sign and get the complete encoded token as a string using the secret
tokenString, err := token.SignedString(key)
return tokenString, err
}
// インストールアクセストークンURLの取得
// https://docs.github.com/ja/apps/creating-github-apps/authenticating-with-a-github-app/authenticating-as-a-github-app-installation
func getGitHubInstallAccessTokenURL(jwt string) (string, error) {
// Use the JWT to authenticate with the GitHub API
// https://docs.github.com/ja/rest/apps/apps?apiVersion=2022-11-28
req, _ := http.NewRequest("GET", "https://api.github.com/app/installations", nil)
req.Header.Set("Accept", "application/vnd.github+json")
req.Header.Set("Authorization", "Bearer "+jwt)
req.Header.Set("X-GitHub-Api-Version", "2022-11-28")
// 結果の取得
jsonByte := doHttp(req)
data := convJsonBytesToMaps(jsonByte)
return data[0]["access_tokens_url"].(string), nil
}
// インストールアクセストークンの取得
//
// (戻り値の例)
// "token": "ghs_xxx"
// "expires_at": "2023-10-31T06:32:37Z"
//
// "permissions":
// "data":
// "contents": "read",
// "metadata": "read",
// "statuses": "read"
//
// "repository_selection": "all"
//
// https://docs.github.com/ja/apps/creating-github-apps/authenticating-with-a-github-app/authenticating-as-a-github-app-installation
func getGitHubInstallAccessToken(jwt string) (map[string]interface{}, error) {
accessTokenUrl, err := getGitHubInstallAccessTokenURL(jwt)
if err != nil {
panic(err)
}
// Use the JWT to authenticate with the GitHub API
// https://docs.github.com/ja/rest/apps/apps?apiVersion=2022-11-28
req, _ := http.NewRequest("POST", accessTokenUrl, nil)
req.Header.Set("Accept", "application/vnd.github+json")
req.Header.Set("Authorization", "Bearer "+jwt)
req.Header.Set("X-GitHub-Api-Version", "2022-11-28")
// 結果の取得
jsonByte := doHttp(req)
data := convJsonByteToMap(jsonByte)
return data, nil
}
// GitHub GraphQL APIの実行
// 引数 query クエリ文字列。改行・タブは自動削除されます。
// Explorer: https://docs.github.com/ja/graphql/overview/explorer
func RunGitHubGQLAPI(query string) (interface{}, error) {
// MEMO: API呼び出しの度にJWTとアクセストークンを再生成している。
// 頻繁にAPIをコールする場合は、処理が無駄なので個別管理する。
jwt, err := generateJWT()
if err != nil {
panic(err)
}
accessToken, err := getGitHubInstallAccessToken(jwt)
if err != nil {
panic(err)
}
// queryから改行・タブを削除する。
queryWithoutNewlines := strings.ReplaceAll(query, "\n", "")
queryWithoutNewlinesAndTabs := strings.ReplaceAll(queryWithoutNewlines, "\t", "")
// Use the AccessTokent to authenticate with the GitHub GraphQL API
// https://docs.github.com/ja/apps/creating-github-apps/authenticating-with-a-github-app/authenticating-as-a-github-app-installation
req, _ := http.NewRequest("POST", "https://api.github.com/graphql", strings.NewReader(queryWithoutNewlinesAndTabs))
req.Header.Set("Accept", "application/vnd.github+json")
req.Header.Set("Authorization", "Bearer "+accessToken["token"].(string))
// 結果の取得
jsonByte := doHttp(req)
var data interface{}
if len(jsonByte) > 0 && jsonByte[0] == '[' {
// データを正常に取得できた場合
data = convJsonBytesToMaps(jsonByte)
} else {
// エラーの場合(返却値が配列ではない)
data = convJsonByteToMap(jsonByte)
}
return data, nil
}
使用go调用GraphQL的示例
我将调用先前的github.go文件中的RunGitHubGQLAPI函数。
import (
"fmt"
"log"
)
func main() {
// GraphQL query
query := `{
"query": "query {
viewer {
repositories(first: 10) {
nodes {
name
description
url
}
pageInfo {
endCursor
hasNextPage
}
}
}
}
"
}`
data, err := RunGitHubGQLAPI(query)
if err != nil {
log.Fatalf("Error fetching GitHub repos: %v", err)
}
fmt.Println(data)
}
总结
我曾经以为调用GraphQL API会更简单,但实际上步骤很繁琐,非常麻烦。
尤其是在进行身份验证实现时,如果出现问题,几乎没有输出有用于错误调查的信息,大多数只会显示“凭证问题”,只能逐一检查每个步骤,这一点相当令人苦恼。
我对学习了一天的golang的感受
-
- main関数があるファイルのpackageはmainではないとだめ。
-
- 外部ファイルから呼び出される関数の名前の先頭は大文字。小文字だと内部のみになる。
-
- ポインタがある。C/C++以外で初めてみた。パフォーマンスをカリカリに詰めるため?
-
- throw/catchがなさそう。昔ながらの戻り値でエラーハンドリングが主流?
-
- やたらinterfaceが出てくる。map[string]interface{}など、まだ馴染めない。
-
- := は初回代入。= は2回目以降の代入。(Delphiでは := で初回、2回目以降の代入どちらも)
-
- requireの参照元GitHubリポジトリが更新されたら、go get で最新コミットを取得しないといけない。
- 関数の引数にデフォルト引数がない。なぜ・・・。
因为 Golang 在周围的评价很好,所以我第一次尝试使用了它,我的印象是它确实是一种轻量/紧凑的语言,就像大家说的那样。
虽然它很轻量/紧凑,但可以实现考虑到性能的处理,并且也有丰富的库,所以对于编写后端处理等想要快速完成的任务很方便。
不过,如果要使用 Golang 进行大规模编码,我不知道如何确保控制一致性。
请查阅文献库。
-
- GitHub公式
-
- ChatGPT先生
- その他、インターネット上のたくさんの記事
最终 (zuì
感谢您阅读到最后。
我们,Kakukaku株式会社,正在寻找与我们一起开发的员工和合作伙伴。
https://kakukaku.app/