注: 以下の翻訳の正確性は検証されていません。AIPを利用して英語版の原文から機械的に翻訳されたものです。
GitHub コネクターは、GitHub 用の Palantir 提供ドライバーです。
新しい GitHub ソースを作成するには、Palantir 提供ドライバーの標準セットアップフローに沿って進め、以下のセクションで GitHub 固有の設定とネットワーク設定を行います。プロパティの完全なリファレンスについては、GitHub ドライバーの公式ドキュメント ↗を参照してください。
| 機能 | ステータス |
|---|---|
| 探索 | 🟢 一般提供 |
| バッチ同期 | 🟢 一般提供 |
| インクリメンタル(差分連携) | 🟢 一般提供 |
| OAuth 2.0 認証 | 🟢 一般提供 |
| テーブルエクスポート | 🟢 一般提供 |
GitHub コネクターは、データの一括読み取りに最適です。リポジトリ、Issue、プルリクエスト、コミット、および関連エンティティからなる GitHub のデータモデルを、リレーショナルテーブルとして公開します。ソースエクスプローラーでこれらの一覧を見て、バッチ同期で Foundry に取り込めます。そのため、下流での分析に向けてリポジトリ全体や組織全体を Foundry にミラーリングする用途に適しています。

GitHub でレコードを作成または更新するには、既存の GitHub ソースを再利用し、外部トランスフォームまたは外部関数から GitHub REST API ↗ を呼び出します。2個目のソースは不要です。以下のコードで GitHub ソースを使用するを参照してください。
このコネクターは OAuth 2.0 を使用して GitHub に認証します。GitHub でアプリケーションを登録し、そのクライアントIDとクライアントシークレットを Foundry ソースに入力します(OAuthClientId / OAuthClientSecret)。InitiateOAuth を REFRESH に設定すると、ドライバーは初回の同期時に GitHub の OAuth Web アプリケーションフロー ↗を実行し、その後もトークンを更新し続けます。
GitHub は2種類のアプリケーションを提供しており、いずれも OAuth 2.0 を基盤とし、同じ方法で設定します。GitHub App(推奨)では、リソースごとのきめ細かな権限(たとえば、単一のリポジトリの Issue に対する読み取り専用アクセス)と、有効期間の短いトークンが追加されます。OAuth アプリは、repo などのアカウントレベルの広範なスコープを使用します。以下の手順では GitHub App を使用します。OAuth アプリでも手順は同じですが、権限をプロビジョニングするワークフローでは、代わりに設定 > 開発者設定から OAuth アプリを選択します。詳しい比較については、GitHub Apps と OAuth アプリの違い ↗を参照してください。
GitHub で、設定 > 開発者設定 > GitHub Apps > 新規 GitHub App ↗ を選択し、コールバック URL を Foundry のソースセットアップページに表示される値に設定します。

権限とイベントで、同期に必要な権限のみ(たとえば、読み取り専用の内容と Issues)を付与し、アプリを作成します。その後、アプリのインストールタブから、対象のリポジトリまたは組織にインストールします。次に、アプリの全般ページでクライアントIDをコピーし、クライアントシークレットを生成します。

Foundry の Data Connection に移動し、クライアントIDとクライアントシークレットをソースの OAuthClientId および OAuthClientSecret プロパティに入力し、InitiateOAuth は REFRESH に設定したままにします。

以下のプロパティは必須または推奨です。
| プロパティ | 必須かどうか | 説明 | デフォルト |
|---|---|---|---|
InitiateOAuth ↗ | 推奨 | OAuth アクセストークンを取得または更新するプロセスを指定します。このトークンは、認証と認可を受けたユーザーの作業中に、ユーザーのアクセスを維持します。 | REFRESH |
OAuthClientId ↗ | 推奨 | カスタム OAuth アプリケーションに割り当てられたクライアントID(コンシューマー鍵とも呼ばれます)を指定します。この ID は、認証時に OAuth 認可サーバーがアプリケーションを識別するために必要です。 | — |
OAuthClientSecret ↗ | 推奨 | カスタム OAuth アプリケーションに割り当てられたクライアントシークレットを指定します。この機密値は、OAuth 認可サーバーに対してアプリケーションを認証するために使用されます。 | — |
以下のテーブルに、ソースの実行に必要なアクセス先のドメインを示します。
ドメインごとに、対応するエグレスポリシーを追加します。ソースがオンプレミスでホストされており、Foundry から直接アクセスできない場合は、代わりに Agent プロキシエグレスポリシーを使用します。Agent ホスト自体も、一覧に記載されたドメインにアクセスできる必要があります。詳細については、Agent をプロキシとして使用するを参照してください。
| ドメイン | 必須 |
|---|---|
| github.com | 常に必須 |
| api.github.com | 常に必須 |
このコネクターは OAuth 2.0 認証をサポートしています。Palantir 提供ドライバーの OAuth 2.0 ガイダンスに沿って、接続を設定して認可します。
このコネクターはテーブルエクスポートをサポートしています。テーブルエクスポートをセットアップする方法を参照してください。
GitHub でレコードを作成または更新する場合や、一括読み取り以外の GitHub REST API ↗ の機能を使用する場合は、外部トランスフォームまたは外部関数から API を呼び出します。既存の GitHub ソースを再利用し、コードで発行した有効期間の短い(約1時間)GitHub App インストールトークンで認証します。2個目のソースは不要です。
次に、ソースを外部トランスフォームまたは関数のリポジトリにインポートします。このソースは組み込みの HTTPS クライアント(get_https_connection().get_client())を公開していませんが、前述のネットワークエグレスポリシーで許可されている api.github.com へのエグレスを継承します。Python の requests ↗ または TypeScript のソースの fetch を使用して、GitHub REST API を直接呼び出せます。
以下の例では、Python または TypeScript を使用し、GitHub App を通じて GitHub の Issue を読み取ります。
このワークフローは OAuth アプリに対応していません。OAuth アプリには個人用アクセストークンが必要です。
まず、Data Connection でソースの接続情報ページに移動し、プロパティを追加 > 新しい暗号化されたプロパティを選択して GitHub App の秘密鍵を追加します。プロパティに Other という名前を付けると、get_secret("Other") で読み取れるようになります。
次に、Python の例の APP_ID を GitHub App の ID またはクライアントIDに設定します。この識別子は公開情報のため、シークレットとして保存する必要はありません。一方、TypeScript の例では、ソースの OAuthClientId プロパティから読み取ります。
ライブプレビューではソースを関数にバインドできないため、これらの例はプレビュー時に必ずエラーになります。代わりに、関数をデプロイして Workshop アプリケーションまたは任意のサーバーレス実行から実行し、動作を確認します。
Copied!1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51import time import jwt import pandas as pd import requests from transforms.api import LightweightOutput, Output, transform from transforms.external.systems import ResolvedSource, Source, external_systems API = "https://api.github.com" APP_ID = "<github_app_id_or_client_id>" OWNER = "<owner>" REPOSITORY = "<repository>" def github_headers(github: ResolvedSource) -> dict[str, str]: now = int(time.time()) app_jwt = jwt.encode( {"iat": now - 60, "exp": now + 540, "iss": APP_ID}, github.get_secret("Other"), algorithm="RS256", ) headers = { "Accept": "application/vnd.github+json", "X-GitHub-Api-Version": "2022-11-28", "Authorization": f"Bearer {app_jwt}", } installation_id = requests.get( f"{API}/repos/{OWNER}/{REPOSITORY}/installation", headers=headers ).json()["id"] token = requests.post( f"{API}/app/installations/{installation_id}/access_tokens", headers=headers, ).json()["token"] return {**headers, "Authorization": f"Bearer {token}"} @external_systems(github=Source("<source_rid>")) @transform.using(output=Output("<issues_output_dataset_rid>")) def get_issues(github: ResolvedSource, output: LightweightOutput) -> None: response = requests.get( f"{API}/repos/{OWNER}/{REPOSITORY}/issues", headers=github_headers(github), params={"state": "all", "per_page": 100}, ) response.raise_for_status() output.write_table(pd.DataFrame([{ "number": issue["number"], "title": issue["title"], "body": issue["body"], "state": issue["state"], } for issue in response.json()]))
Copied!1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53import time from dataclasses import dataclass import jwt import requests from functions.api import function from functions.sources import get_source API = "https://api.github.com" APP_ID = "<github_app_id_or_client_id>" SOURCE_ALIAS = "<source_alias>" @dataclass class GitHubIssue: number: int title: str body: str | None state: str def github_headers(github, owner: str, repository: str) -> dict[str, str]: now = int(time.time()) app_jwt = jwt.encode( {"iat": now - 60, "exp": now + 540, "iss": APP_ID}, github.get_secret("Other"), algorithm="RS256", ) headers = { "Accept": "application/vnd.github+json", "X-GitHub-Api-Version": "2022-11-28", "Authorization": f"Bearer {app_jwt}", } installation_id = requests.get( f"{API}/repos/{owner}/{repository}/installation", headers=headers ).json()["id"] token = requests.post( f"{API}/app/installations/{installation_id}/access_tokens", headers=headers, ).json()["token"] return {**headers, "Authorization": f"Bearer {token}"} @function(sources=["<source_alias>"]) def get_issues(owner: str, repository: str) -> list[GitHubIssue]: response = requests.get( f"{API}/repos/{owner}/{repository}/issues", headers=github_headers(get_source(SOURCE_ALIAS), owner, repository), params={"state": "all", "per_page": 100}, ) response.raise_for_status() return [GitHubIssue(issue["number"], issue["title"], issue["body"], issue["state"]) for issue in response.json()]
Copied!1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53import { ExternalSystems, Function, Integer } from "@foundry/functions-api"; import { GitHubSource } from "@foundry/external-systems/sources"; import * as jwt from "jsonwebtoken"; const API = "https://api.github.com"; export interface GitHubIssue { number: Integer; title: string; body?: string; state: string; } async function githubHeaders(owner: string, repository: string): Promise<Record<string, string>> { const now = Math.floor(Date.now() / 1000); const appJwt = jwt.sign( { iat: now - 60, exp: now + 540, iss: GitHubSource.getSecret("OAuthClientId") }, GitHubSource.getSecret("Other"), { algorithm: "RS256" }, ); const headers = { Accept: "application/vnd.github+json", "X-GitHub-Api-Version": "2022-11-28", Authorization: `Bearer ${appJwt}`, }; const installation = await GitHubSource.fetch( `${API}/repos/${owner}/${repository}/installation`, { method: "GET", headers } ); const { id } = await installation.json() as { id: number }; const tokenResponse = await GitHubSource.fetch( `${API}/app/installations/${id}/access_tokens`, { method: "POST", headers } ); const { token } = await tokenResponse.json() as { token: string }; return { ...headers, Authorization: `Bearer ${token}` }; } export class GitHubFunctions { @ExternalSystems({ sources: [GitHubSource] }) @Function() public async getIssues(owner: string, repository: string): Promise<GitHubIssue[]> { const response = await GitHubSource.fetch( `${API}/repos/${owner}/${repository}/issues?state=all&per_page=100`, { method: "GET", headers: await githubHeaders(owner, repository) }, ); if (!response.ok) throw new Error(`GitHub returned ${response.status}`); const issues = await response.json() as Array<{ number: number; title: string; body: string | null; state: string; }>; return issues.map(({ number, title, body, state }) => ({ number, title, body: body ?? undefined, state, })); } }
Copied!1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54import { Integer } from "@osdk/functions"; import { getFetch, getSource } from "@palantir/functions-sources"; import jwt from "jsonwebtoken"; const API = "https://api.github.com"; const SOURCE_RID = "<source_rid>"; export const config = { sources: [SOURCE_RID] }; export interface GitHubIssue { number: Integer; title: string; body?: string; state: string; } async function githubClient(owner: string, repository: string) { const source = await getSource({ rid: SOURCE_RID }); const request = await getFetch(source); const now = Math.floor(Date.now() / 1000); const appJwt = jwt.sign( { iat: now - 60, exp: now + 540, iss: source.secrets.OAuthClientId! }, source.secrets.Other!, { algorithm: "RS256" }, ); const headers = { Accept: "application/vnd.github+json", "X-GitHub-Api-Version": "2022-11-28", Authorization: `Bearer ${appJwt}`, }; const installation = await request( `${API}/repos/${owner}/${repository}/installation`, { method: "GET", headers } ); const { id } = await installation.json() as { id: number }; const tokenResponse = await request( `${API}/app/installations/${id}/access_tokens`, { method: "POST", headers } ); const { token } = await tokenResponse.json() as { token: string }; return { request, headers: { ...headers, Authorization: `Bearer ${token}` } }; } export default async function getIssues(owner: string, repository: string): Promise<GitHubIssue[]> { const { request, headers } = await githubClient(owner, repository); const response = await request( `${API}/repos/${owner}/${repository}/issues?state=all&per_page=100`, { method: "GET", headers }, ); if (!response.ok) throw new Error(`GitHub returned ${response.status}`); const issues = await response.json() as Array<{ number: number; title: string; body: string | null; state: string; }>; return issues.map(({ number, title, body, state }) => ({ number, title, body: body ?? undefined, state, })); }
以下の例では、GitHub の Issue を読み取る例と同じ GitHub App の認証フローを使用して、GitHub の Issue の本文を更新します。
各例は、選択した Issue の本文全体を即座に置き換えます。本文を上書きする予定の Issue のみを指定してください。
Copied!1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51import time import jwt import pandas as pd import requests from transforms.api import LightweightOutput, Output, transform from transforms.external.systems import ResolvedSource, Source, external_systems API = "https://api.github.com" APP_ID = "<github_app_id_or_client_id>" OWNER = "<owner>" REPOSITORY = "<repository>" ISSUE_NUMBER = "<issue_number>" UPDATED_BODY = "<updated_issue_body>" def github_headers(github: ResolvedSource) -> dict[str, str]: now = int(time.time()) app_jwt = jwt.encode( {"iat": now - 60, "exp": now + 540, "iss": APP_ID}, github.get_secret("Other"), algorithm="RS256", ) headers = { "Accept": "application/vnd.github+json", "X-GitHub-Api-Version": "2022-11-28", "Authorization": f"Bearer {app_jwt}", } installation_id = requests.get( f"{API}/repos/{OWNER}/{REPOSITORY}/installation", headers=headers ).json()["id"] token = requests.post( f"{API}/app/installations/{installation_id}/access_tokens", headers=headers, ).json()["token"] return {**headers, "Authorization": f"Bearer {token}"} @external_systems(github=Source("<source_rid>")) @transform.using(output=Output("<update_output_dataset_rid>")) def patch_issue(github: ResolvedSource, output: LightweightOutput) -> None: response = requests.patch( f"{API}/repos/{OWNER}/{REPOSITORY}/issues/{ISSUE_NUMBER}", headers=github_headers(github), json={"body": UPDATED_BODY}, ) response.raise_for_status() output.write_table(pd.DataFrame([{ "issue_number": ISSUE_NUMBER, "body": UPDATED_BODY, }]))
Copied!1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45import time import jwt import requests from functions.api import function from functions.sources import get_source API = "https://api.github.com" APP_ID = "<github_app_id_or_client_id>" SOURCE_ALIAS = "<source_alias>" def github_headers(github, owner: str, repository: str) -> dict[str, str]: now = int(time.time()) app_jwt = jwt.encode( {"iat": now - 60, "exp": now + 540, "iss": APP_ID}, github.get_secret("Other"), algorithm="RS256", ) headers = { "Accept": "application/vnd.github+json", "X-GitHub-Api-Version": "2022-11-28", "Authorization": f"Bearer {app_jwt}", } installation_id = requests.get( f"{API}/repos/{owner}/{repository}/installation", headers=headers ).json()["id"] token = requests.post( f"{API}/app/installations/{installation_id}/access_tokens", headers=headers, ).json()["token"] return {**headers, "Authorization": f"Bearer {token}"} @function(sources=["<source_alias>"]) def patch_issue( owner: str, repository: str, issue_number: int, body: str ) -> str: response = requests.patch( f"{API}/repos/{owner}/{repository}/issues/{issue_number}", headers=github_headers(get_source(SOURCE_ALIAS), owner, repository), json={"body": body}, ) response.raise_for_status() return body
Copied!1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47import { ExternalSystems, Function, Integer } from "@foundry/functions-api"; import { GitHubSource } from "@foundry/external-systems/sources"; import * as jwt from "jsonwebtoken"; const API = "https://api.github.com"; async function githubHeaders(owner: string, repository: string): Promise<Record<string, string>> { const now = Math.floor(Date.now() / 1000); const appJwt = jwt.sign( { iat: now - 60, exp: now + 540, iss: GitHubSource.getSecret("OAuthClientId") }, GitHubSource.getSecret("Other"), { algorithm: "RS256" }, ); const headers = { Accept: "application/vnd.github+json", "X-GitHub-Api-Version": "2022-11-28", Authorization: `Bearer ${appJwt}`, }; const installation = await GitHubSource.fetch( `${API}/repos/${owner}/${repository}/installation`, { method: "GET", headers } ); const { id } = await installation.json() as { id: number }; const tokenResponse = await GitHubSource.fetch( `${API}/app/installations/${id}/access_tokens`, { method: "POST", headers } ); const { token } = await tokenResponse.json() as { token: string }; return { ...headers, Authorization: `Bearer ${token}` }; } export class GitHubFunctions { @ExternalSystems({ sources: [GitHubSource] }) @Function() public async patchIssue( owner: string, repository: string, issueNumber: Integer, body: string ): Promise<string> { const response = await GitHubSource.fetch( `${API}/repos/${owner}/${repository}/issues/${issueNumber}`, { method: "PATCH", headers: { ...await githubHeaders(owner, repository), "Content-Type": "application/json" }, body: JSON.stringify({ body }), }, ); if (!response.ok) throw new Error(`GitHub returned ${response.status}`); return body; } }
Copied!1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48import { Integer } from "@osdk/functions"; import { getFetch, getSource } from "@palantir/functions-sources"; import jwt from "jsonwebtoken"; const API = "https://api.github.com"; const SOURCE_RID = "<source_rid>"; export const config = { sources: [SOURCE_RID] }; async function githubClient(owner: string, repository: string) { const source = await getSource({ rid: SOURCE_RID }); const request = await getFetch(source); const now = Math.floor(Date.now() / 1000); const appJwt = jwt.sign( { iat: now - 60, exp: now + 540, iss: source.secrets.OAuthClientId! }, source.secrets.Other!, { algorithm: "RS256" }, ); const headers = { Accept: "application/vnd.github+json", "X-GitHub-Api-Version": "2022-11-28", Authorization: `Bearer ${appJwt}`, }; const installation = await request( `${API}/repos/${owner}/${repository}/installation`, { method: "GET", headers } ); const { id } = await installation.json() as { id: number }; const tokenResponse = await request( `${API}/app/installations/${id}/access_tokens`, { method: "POST", headers } ); const { token } = await tokenResponse.json() as { token: string }; return { request, headers: { ...headers, Authorization: `Bearer ${token}` } }; } export default async function patchIssue( owner: string, repository: string, issueNumber: Integer, body: string ): Promise<string> { const { request, headers } = await githubClient(owner, repository); const response = await request( `${API}/repos/${owner}/${repository}/issues/${issueNumber}`, { method: "PATCH", headers: { ...headers, "Content-Type": "application/json" }, body: JSON.stringify({ body }), }, ); if (!response.ok) throw new Error(`GitHub returned ${response.status}`); return body; }