GitHub app installation token

This commit is contained in:
Tienson Qin
2026-02-27 23:39:13 +08:00
parent 7b0bd2d07b
commit c8ecc7d191
8 changed files with 487 additions and 123 deletions

View File

@@ -180,7 +180,10 @@ Cloudflare runtime flow:
| CLOUDFLARE_REPO_CLONE_COMMAND | Optional repo clone command template for Cloudflare sandbox |
| CLOUDFLARE_HEALTH_RETRIES | Cloudflare sandbox health check retry count |
| CLOUDFLARE_HEALTH_INTERVAL_MS | Cloudflare sandbox health check retry interval (ms) |
| GITHUB_TOKEN | Fallback token used for both git push and PR API calls |
| GITHUB_APP_ID | GitHub App ID used to mint installation tokens |
| GITHUB_APP_INSTALLATION_ID | Optional fixed installation ID (if omitted, resolved from repo) |
| GITHUB_APP_PRIVATE_KEY | GitHub App private key PEM used for JWT signing |
| GITHUB_APP_SLUG | Optional app slug used to build install URL in setup prompts |
| GITHUB_API_BASE | Optional GitHub API base URL override (default `https://api.github.com`) |
| OPENAI_API_KEY | Passed into Cloudflare sandbox runtime env (if set) |
| ANTHROPIC_API_KEY | Passed into Cloudflare sandbox runtime env (if set) |
@@ -197,15 +200,24 @@ This endpoint is available to any authenticated collaborator and supports:
- push only (`{"create-pr": false}`)
- push + PR (`{"create-pr": true}` or omitted)
When creating a session, the worker verifies the GitHub App is installed on the target repo.
If not installed, `POST /sessions` returns `412` with an install prompt message.
Agent chat messages can also trigger the same publish flow:
- `push`
- `submit PR`
- natural language requests like `please push this branch` or `can you submit a pull request?`
Response `status` values:
- `pushed`
- `pr-created`
- `manual-pr-required`
If PR credentials are missing or PR API creation fails after a successful push, response includes `manual-pr-url`.
If PR API creation fails after a successful push, response includes `manual-pr-url`.
For Cloudflare deploys, store tokens as agents worker secrets:
- `wrangler secret put GITHUB_TOKEN -c worker/wrangler.agents.toml --env <staging|prod>`
- `wrangler secret put GITHUB_APP_PRIVATE_KEY -c worker/wrangler.agents.toml --env <staging|prod>`
- set `GITHUB_APP_ID` and optional `GITHUB_APP_INSTALLATION_ID` in worker vars
## Notes
- Protocol definitions live in `docs/agent-guide/db-sync/protocol.md`.

View File

@@ -267,21 +267,17 @@
[]
"main")
(defn- github-default-branch-token
[^js env]
(or (source-control/push-token env)
(source-control/pr-token env)))
(defn- github-branches-token
[^js env]
(or (source-control/push-token env)
(source-control/pr-token env)))
(defn- task-requested-base-branch
[task]
(or (some-> (get-in task [:project :base-branch]) source-control/sanitize-branch-name)
(some-> (get-in task [:project :branch]) source-control/sanitize-branch-name)))
(defn- github-install-required-message
[install-url]
(if (string? install-url)
(str "GitHub App is not installed for this repository. Install it and retry: " install-url)
"GitHub App is not installed for this repository. Install it and retry."))
(defn- <ensure-task-base-branch!
[^js env task]
(let [repo-url (some-> (get-in task [:project :repo-url]) str string/trim not-empty)
@@ -301,7 +297,7 @@
:else
(p/let [detected-base (source-control/<default-branch! env
(github-default-branch-token env)
nil
repo-url)
detected-base (source-control/sanitize-branch-name detected-base)
fallback-base (source-control/sanitize-branch-name (default-base-branch))
@@ -567,24 +563,36 @@
(http/forbidden)
:else
(p/let [task (<ensure-task-base-branch! (.-env self) task)]
(let [session (session/initial-session task audit now)
[session events _event] (session/append-event session [] {:type "session.created"
:data {:requested-by user-id
:project (:project task)
:agent (:agent task)}
:ts now})]
(p/let [_ (<put-session! self session)
_ (<put-events! self events)
_ (<provision-runtime! self task task-id)
updated-session (<get-session self)]
(http/json-response :sessions/create
{:session-id task-id
:status (or (:status updated-session)
(:status session))
:runtime-provider (session-runtime-provider updated-session)
:terminal-enabled (session-terminal-enabled? updated-session)
:stream-url (stream-url request task-id)}))))))))))))
(p/let [repo-url (some-> (get-in task [:project :repo-url]) str string/trim not-empty)
install-status (-> (if (string? repo-url)
(source-control/<repo-installation-status! (.-env self) repo-url)
(p/resolved {:installed? true}))
(p/catch (fn [error]
{:error error})))]
(if (:error install-status)
(http/error-response "failed to verify GitHub App installation" 500)
(if (and (map? install-status)
(false? (:installed? install-status)))
(http/error-response (github-install-required-message (:install-url install-status)) 412)
(p/let [task (<ensure-task-base-branch! (.-env self) task)]
(let [session (session/initial-session task audit now)
[session events _event]
(session/append-event session [] {:type "session.created"
:data {:requested-by user-id
:project (:project task)
:agent (:agent task)}
:ts now})]
(p/let [_ (<put-session! self session)
_ (<put-events! self events)
_ (<provision-runtime! self task task-id)
updated-session (<get-session self)]
(http/json-response :sessions/create
{:session-id task-id
:status (or (:status updated-session)
(:status session))
:runtime-provider (session-runtime-provider updated-session)
:terminal-enabled (session-terminal-enabled? updated-session)
:stream-url (stream-url request task-id)})))))))))))))))
(defn- handle-status [^js self _request]
(p/let [session (<get-session self)]
@@ -741,12 +749,11 @@
(defn- <handle-pr-after-push!
[^js self current-session body user-id repo-url head-branch force? create-pr?]
(p/let [pr-token (source-control/pr-token (.-env self))
requested-base-branch (source-control/sanitize-branch-name (:base-branch body))
(p/let [requested-base-branch (source-control/sanitize-branch-name (:base-branch body))
default-base (source-control/sanitize-branch-name (default-base-branch))
detected-base-branch (when (nil? requested-base-branch)
(source-control/<default-branch! (.-env self)
pr-token
nil
repo-url))
detected-base-branch (source-control/sanitize-branch-name detected-base-branch)
base-branch (or requested-base-branch
@@ -774,21 +781,13 @@
(http/forbidden)
:else
(let [description (or (some-> (:body body) str string/trim not-empty)
"Automated changes from agent session.")
title (or (some-> (:title body) str string/trim not-empty)
(description->pr-title description)
(str "Agent updates for session " (:id current-session)))
manual-url (source-control/manual-pr-url repo-url head-branch base-branch)]
(if-not (string? pr-token)
(<manual-pr-required-response! self
{:user-id user-id
:head-branch head-branch
:base-branch base-branch
:manual-url manual-url
:reason "missing-token"
:force? force?
:message "branch pushed; create pull request manually"})
(p/let [pr-token (source-control/<pr-token! (.-env self) repo-url)]
(let [description (or (some-> (:body body) str string/trim not-empty)
"Automated changes from agent session.")
title (or (some-> (:title body) str string/trim not-empty)
(description->pr-title description)
(str "Agent updates for session " (:id current-session)))
manual-url (source-control/manual-pr-url repo-url head-branch base-branch)]
(<create-pr-response! self
{:user-id user-id
:repo-url repo-url
@@ -803,13 +802,14 @@
(defn- <perform-pr-push!
[^js self current-session body user-id repo-url runtime force? create-pr? push-branch-fn]
(let [provider (runtime-provider/resolve-provider (.-env self) runtime)
push-token (source-control/push-token (.-env self))
env (.-env self)
commit-message (some-> (:commit-message body) str string/trim not-empty)
head-branch (source-control/resolve-head-branch (:head-branch body)
(generated-head-branch current-session body))]
(if-not (string? head-branch)
(http/bad-request "invalid head branch")
(-> (p/let [_ (<append-publish-event! self "git.push.started"
(-> (p/let [push-token (source-control/<push-token! env repo-url)
_ (<append-publish-event! self "git.push.started"
{:by user-id
:head-branch head-branch
:force force?})
@@ -1084,8 +1084,9 @@
(repo-url-from-session session))]
(if-not (string? repo-url)
(http/bad-request "missing repo url")
(p/let [branches (source-control/<list-branches! env
(github-branches-token env)
(p/let [token (source-control/<push-token! env repo-url)
branches (source-control/<list-branches! env
token
repo-url)]
(http/json-response :sessions/branches
{:branches (->> branches

View File

@@ -98,21 +98,154 @@
(or (some-> (aget env "GITHUB_USER_AGENT") non-empty-str)
"logseq-worker/1.0"))
(defn pr-token
[^js env]
(some-> (aget env "GITHUB_TOKEN") non-empty-str))
(def ^:private github-api-version "2022-11-28")
(def ^:private token-refresh-skew-ms 60000)
(defonce ^:private installation-token-cache (atom {}))
(defn push-token
[^js env]
(some-> (aget env "GITHUB_TOKEN") non-empty-str))
(defn- now-ms []
(.now js/Date))
(defn push-remote-url
[repo-url token]
(when-let [{:keys [provider owner name]} (repo-ref repo-url)]
(when (= "github" provider)
(if (string? token)
(str "https://x-access-token:" (js/encodeURIComponent token) "@github.com/" owner "/" name ".git")
(str "https://github.com/" owner "/" name ".git")))))
(defn- parse-int-safe
[value]
(let [num (some-> value js/parseInt)]
(when (and (number? num)
(not (js/isNaN num)))
num)))
(defn- parse-time-ms
[value]
(let [ms (some-> value js/Date.parse)]
(when (and (number? ms)
(not (js/isNaN ms)))
ms)))
(defn- base64url
[value]
(-> (or value "")
(string/replace #"\+" "-")
(string/replace #"/" "_")
(string/replace #"=+$" "")))
(defn- uint8->base64url
[payload]
(base64url (js/btoa (apply str (map char payload)))))
(defn- text->base64url
[text]
(-> (.encode (js/TextEncoder.) (or text ""))
uint8->base64url))
(defn- json->base64url
[payload]
(text->base64url (js/JSON.stringify (clj->js payload))))
(defn- github-app-id
[^js env]
(some-> (aget env "GITHUB_APP_ID") non-empty-str))
(defn- github-app-installation-id
[^js env]
(some-> (aget env "GITHUB_APP_INSTALLATION_ID")
non-empty-str
parse-int-safe))
(defn- github-app-private-key
[^js env]
(some-> (aget env "GITHUB_APP_PRIVATE_KEY")
non-empty-str
(string/replace #"\\n" "\n")))
(defn- github-app-slug
[^js env]
(or (some-> (aget env "GITHUB_APP_SLUG") non-empty-str)
(some-> (aget env "GITHUB_APP_NAME") non-empty-str)))
(defn- node-subtle
[]
(let [require-fn (aget js/globalThis "require")]
(when (fn? require-fn)
(let [crypto-module (.call require-fn nil "node:crypto")
webcrypto (when crypto-module
(aget crypto-module "webcrypto"))]
(when webcrypto
(aget webcrypto "subtle"))))))
(defn- subtle-crypto
[]
(or (some-> (aget js/globalThis "crypto")
.-subtle)
(node-subtle)))
(defn- pem->array-buffer
[pem]
(let [body (-> (or pem "")
(string/replace #"-+BEGIN[^-]+-+" "")
(string/replace #"-+END[^-]+-+" "")
(string/replace #"\s+" ""))
binary (js/atob body)
length (.-length binary)
payload (js/Uint8Array. length)]
(dotimes [idx length]
(aset payload idx (.charCodeAt binary idx)))
(.-buffer payload)))
(defn- <import-private-key!
[private-key]
(if-let [subtle (subtle-crypto)]
(.importKey subtle
"pkcs8"
(pem->array-buffer private-key)
#js {:name "RSASSA-PKCS1-v1_5"
:hash #js {:name "SHA-256"}}
false
#js ["sign"])
(p/rejected (ex-info "webcrypto subtle unavailable"
{:reason :crypto-unavailable}))))
(defn- <github-app-jwt!
[^js env]
(let [app-id-raw (github-app-id env)
app-id (or (parse-int-safe app-id-raw) app-id-raw)
private-key (github-app-private-key env)]
(cond
(nil? app-id)
(p/rejected (ex-info "missing GITHUB_APP_ID"
{:reason :missing-github-app-auth}))
(not (string? private-key))
(p/rejected (ex-info "missing GITHUB_APP_PRIVATE_KEY"
{:reason :missing-github-app-auth}))
:else
(let [issued-at (js/Math.floor (/ (now-ms) 1000))
header (json->base64url {:alg "RS256"
:typ "JWT"})
claims (json->base64url {:iat (- issued-at 60)
:exp (+ issued-at 540)
:iss app-id})
unsigned-token (str header "." claims)
encoded-token (.encode (js/TextEncoder.) unsigned-token)]
(p/let [private-key-obj (<import-private-key! private-key)
subtle (subtle-crypto)
signature (.sign subtle
#js {:name "RSASSA-PKCS1-v1_5"}
private-key-obj
encoded-token)
signature-token (-> (js/Uint8Array. signature)
uint8->base64url)]
(str unsigned-token "." signature-token))))))
(defn- request-headers
[^js env {:keys [token content-type]}]
(let [headers (js/Headers.)]
(.set headers "accept" "application/vnd.github+json")
(.set headers "user-agent" (user-agent env))
(.set headers "x-github-api-version" github-api-version)
(when-let [auth-token (non-empty-str token)]
(.set headers "authorization" (str "Bearer " auth-token)))
(when-let [value (non-empty-str content-type)]
(.set headers "content-type" value))
headers))
(defn- parse-json-safe
[text]
@@ -144,18 +277,182 @@
{}
github-debug-response-headers))
(defn- fetch-json-safe!
[url request]
(p/let [resp (js/fetch url request)
status (.-status resp)
text (.text resp)
payload (parse-json-safe text)
response-headers (select-response-headers resp)]
{:status status
:ok? (<= 200 status 299)
:payload payload
:raw-body text
:response-headers response-headers}))
(defn- token-cache-key
[^js env repo-url installation-id]
(str (api-base-url env) "|" (or installation-id repo-url)))
(defn- cached-installation-token
[cache-key]
(let [{:keys [token expires-at-ms]} (get @installation-token-cache cache-key)]
(when (and (string? token)
(number? expires-at-ms)
(> (- expires-at-ms (now-ms)) token-refresh-skew-ms))
token)))
(defn- <resolve-installation-id!
[^js env app-jwt repo-url]
(if-let [installation-id (github-app-installation-id env)]
(p/resolved installation-id)
(if-let [{:keys [provider owner name]} (repo-ref repo-url)]
(if-not (= "github" provider)
(p/rejected (ex-info "unsupported scm provider"
{:reason :unsupported-provider
:provider provider}))
(let [url (str (api-base-url env) "/repos/" owner "/" name "/installation")
headers (request-headers env {:token app-jwt})]
(p/let [{:keys [ok? status payload raw-body response-headers]}
(fetch-json-safe! url #js {:method "GET" :headers headers})
installation-id (some-> (:id payload) parse-int-safe)]
(if (and ok? (number? installation-id))
installation-id
(p/rejected (ex-info "resolve installation failed"
{:reason :installation-resolution-failed
:status status
:body payload
:raw-body raw-body
:response-headers response-headers}))))))
(p/rejected (ex-info "invalid repo url"
{:reason :invalid-repo-url
:repo-url repo-url})))))
(defn <installation-token!
[^js env repo-url]
(if-not (map? (repo-ref repo-url))
(p/rejected (ex-info "invalid repo url"
{:reason :invalid-repo-url
:repo-url repo-url}))
(let [configured-installation-id (github-app-installation-id env)
initial-cache-key (token-cache-key env repo-url configured-installation-id)
cached-token (cached-installation-token initial-cache-key)]
(if (string? cached-token)
(p/resolved cached-token)
(p/let [app-jwt (<github-app-jwt! env)
installation-id (<resolve-installation-id! env app-jwt repo-url)
cache-key (token-cache-key env repo-url installation-id)
cached-token (cached-installation-token cache-key)]
(if (string? cached-token)
cached-token
(let [url (str (api-base-url env) "/app/installations/" installation-id "/access_tokens")
headers (request-headers env {:token app-jwt
:content-type "application/json"})]
(p/let [{:keys [ok? status payload raw-body response-headers]}
(fetch-json-safe! url
#js {:method "POST"
:headers headers
:body "{}"})
token (:token payload)
expires-at-ms (some-> (:expires_at payload) parse-time-ms)]
(if (and ok?
(string? token)
(number? expires-at-ms))
(do
(swap! installation-token-cache
assoc
cache-key
{:token token
:expires-at-ms expires-at-ms})
token)
(p/rejected (ex-info "create installation token failed"
{:reason :token-mint-failed
:status status
:body payload
:raw-body raw-body
:response-headers response-headers})))))))))))
(defn app-install-url
[^js env repo-url]
(when-let [{:keys [provider]} (repo-ref repo-url)]
(when (= "github" provider)
(some-> (github-app-slug env)
(#(str "https://github.com/apps/" % "/installations/new"))))))
(defn <repo-installation-status!
[^js env repo-url]
(if-let [{:keys [provider owner name]} (repo-ref repo-url)]
(if-not (= "github" provider)
(p/resolved {:provider provider
:installed? true
:check-required? false})
(p/let [app-jwt (<github-app-jwt! env)
url (str (api-base-url env) "/repos/" owner "/" name "/installation")
headers (request-headers env {:token app-jwt})
{:keys [ok? status payload raw-body response-headers]}
(fetch-json-safe! url #js {:method "GET" :headers headers})
installation-id (some-> (:id payload) parse-int-safe)]
(cond
(and ok? (number? installation-id))
{:provider provider
:installed? true
:check-required? true
:installation-id installation-id}
(= status 404)
{:provider provider
:installed? false
:check-required? true
:reason :not-installed
:install-url (app-install-url env repo-url)}
:else
(p/rejected (ex-info "check github app installation failed"
{:reason :installation-check-failed
:status status
:body payload
:raw-body raw-body
:response-headers response-headers})))))
(p/resolved {:provider nil
:installed? true
:check-required? false})))
(defn pr-token
[_env]
nil)
(defn push-token
[_env]
nil)
(defn <pr-token!
[^js env repo-url]
(<installation-token! env repo-url))
(defn <push-token!
[^js env repo-url]
(<installation-token! env repo-url))
(defn push-remote-url
[repo-url token]
(when-let [{:keys [provider owner name]} (repo-ref repo-url)]
(when (and (= "github" provider)
(string? token))
(str "https://x-access-token:"
(js/encodeURIComponent token)
"@github.com/"
owner
"/"
name
".git"))))
(defn <default-branch!
[^js env token repo-url]
(if-let [{:keys [provider owner name]} (repo-ref repo-url)]
(if-not (= "github" provider)
(p/resolved nil)
(let [url (str (api-base-url env) "/repos/" owner "/" name)
headers (doto (js/Headers.)
(.set "accept" "application/vnd.github+json")
(.set "user-agent" (user-agent env))
(.set "x-github-api-version" "2022-11-28"))
_ (when (string? token)
(.set headers "authorization" (str "Bearer " token)))]
headers (request-headers env {:token token})]
(p/let [resp (js/fetch url #js {:method "GET" :headers headers})
status (.-status resp)
text (.text resp)
@@ -171,12 +468,7 @@
(if-not (= "github" provider)
(p/resolved [])
(let [url (str (api-base-url env) "/repos/" owner "/" name "/branches?per_page=100")
headers (doto (js/Headers.)
(.set "accept" "application/vnd.github+json")
(.set "user-agent" (user-agent env))
(.set "x-github-api-version" "2022-11-28"))
_ (when (string? token)
(.set headers "authorization" (str "Bearer " token)))]
headers (request-headers env {:token token})]
(p/let [resp (js/fetch url #js {:method "GET" :headers headers})
status (.-status resp)
text (.text resp)
@@ -195,7 +487,7 @@
[^js env token repo-url {:keys [title body head-branch base-branch draft]}]
(cond
(not (string? token))
(p/rejected (ex-info "missing github token" {:reason :missing-token}))
(p/rejected (ex-info "missing github app installation token" {:reason :missing-token}))
(not (string? title))
(p/rejected (ex-info "missing pull request title" {:reason :invalid-request}))
@@ -214,12 +506,8 @@
:head (or head-branch "")
:base (or base-branch "main")}
(true? draft) (assoc :draft true))
headers (doto (js/Headers.)
(.set "accept" "application/vnd.github+json")
(.set "content-type" "application/json")
(.set "authorization" (str "Bearer " token))
(.set "user-agent" (user-agent env))
(.set "x-github-api-version" "2022-11-28"))]
headers (request-headers env {:token token
:content-type "application/json"})]
(p/let [resp (js/fetch url #js {:method "POST"
:headers headers
:body (js/JSON.stringify (clj->js payload))})
@@ -257,11 +545,7 @@
(string? base-branch)
(str "&base=" (js/encodeURIComponent base-branch)))
url (str (api-base-url env) "/repos/" owner "/" name "/pulls?" query)
headers (doto (js/Headers.)
(.set "accept" "application/vnd.github+json")
(.set "authorization" (str "Bearer " token))
(.set "user-agent" (user-agent env))
(.set "x-github-api-version" "2022-11-28"))]
headers (request-headers env {:token token})]
(p/let [resp (js/fetch url #js {:method "GET" :headers headers})
status (.-status resp)
text (.text resp)

View File

@@ -44,7 +44,10 @@
:cloudflare-repo-clone-command (env-value env "CLOUDFLARE_REPO_CLONE_COMMAND")
:cloudflare-health-retries (env-value env "CLOUDFLARE_HEALTH_RETRIES")
:cloudflare-health-interval-ms (env-value env "CLOUDFLARE_HEALTH_INTERVAL_MS")
:github-token (env-value env "GITHUB_TOKEN")
:github-app-id (env-value env "GITHUB_APP_ID")
:github-app-installation-id (env-value env "GITHUB_APP_INSTALLATION_ID")
:github-app-private-key (env-value env "GITHUB_APP_PRIVATE_KEY")
:github-app-slug (env-value env "GITHUB_APP_SLUG")
:github-api-base (env-value env "GITHUB_API_BASE")
:openai-api-key (env-value env "OPENAI_API_KEY")
:anthropic-api-key (env-value env "ANTHROPIC_API_KEY")
@@ -66,7 +69,7 @@
:cloudflare-sandbox-name-prefix :cloudflare-sandbox-agent-port
:cloudflare-bootstrap-command :cloudflare-repo-clone-command
:cloudflare-health-retries :cloudflare-health-interval-ms
:github-token :github-api-base
:github-app-id :github-app-installation-id :github-app-private-key :github-app-slug :github-api-base
:openai-api-key :anthropic-api-key :openai-base-url :anthropic-base-url
:log-level :cognito-issuer :cognito-client-id :cognito-jwks-url])

View File

@@ -53,7 +53,10 @@
(aset "CLOUDFLARE_REPO_CLONE_COMMAND" (:cloudflare-repo-clone-command cfg))
(aset "CLOUDFLARE_HEALTH_RETRIES" (:cloudflare-health-retries cfg))
(aset "CLOUDFLARE_HEALTH_INTERVAL_MS" (:cloudflare-health-interval-ms cfg))
(aset "GITHUB_TOKEN" (:github-token cfg))
(aset "GITHUB_APP_ID" (:github-app-id cfg))
(aset "GITHUB_APP_INSTALLATION_ID" (:github-app-installation-id cfg))
(aset "GITHUB_APP_PRIVATE_KEY" (:github-app-private-key cfg))
(aset "GITHUB_APP_SLUG" (:github-app-slug cfg))
(aset "GITHUB_API_BASE" (:github-api-base cfg))
(aset "OPENAI_API_KEY" (:openai-api-key cfg))
(aset "ANTHROPIC_API_KEY" (:anthropic-api-key cfg))

View File

@@ -844,8 +844,6 @@
:repo-url (:repo-url opts)
:force (:force opts)
:remote "origin"}))
source-control/pr-token
(fn [_env] "token-1")
source-control/<default-branch!
(fn [_env _token _repo-url]
(js/Promise.resolve same-branch))]

View File

@@ -1,4 +1,7 @@
AGENT_RUNTIME_PROVIDER=cloudflare
SANDBOX_AGENT_TOKEN=${SANDBOX_AGENT_TOKEN}
SPRITE_TOKEN=${SPRITE_TOKEN}
GITHUB_TOKEN=${GITHUB_TOKEN}
GITHUB_APP_ID=${GITHUB_APP_ID}
GITHUB_APP_INSTALLATION_ID=${GITHUB_APP_INSTALLATION_ID}
GITHUB_APP_PRIVATE_KEY=${GITHUB_APP_PRIVATE_KEY}
GITHUB_APP_SLUG=${GITHUB_APP_SLUG}

View File

@@ -11,6 +11,7 @@
[frontend.state :as state]
[frontend.util :as util]
[lambdaisland.glogi :as log]
[logseq.shui.ui :as shui]
[logseq.sync.malli-schema :as db-sync-schema]
[promesa.core :as p]))
@@ -114,6 +115,55 @@
(re-matches #"^ssh://git@github\.com/([^/]+)/([^/]+?)(?:\.git)?/?$"))]
{:owner owner :name repo}))))
(def ^:private github-install-required-notification-uid :agent/github-install-required)
(defn- first-url-in-text
[text]
(some-> (when (string? text)
(re-find #"https?://[^\s)\]}]+" text))
(string/replace #"[,.;:!?]+$" "")))
(defn- start-session-error-message
[error]
(or (some-> (ex-data error) :body :error)
(some-> error ex-message)
"Failed to start agent session."))
(defn- start-session-install-url
[error]
(first-url-in-text (some-> (ex-data error) :body :error)))
(defn- open-external-url!
[url]
(when (string? url)
(if (util/electron?)
(js/window.apis.openExternal url)
(js/window.open url "_blank" "noopener,noreferrer"))))
(defn- show-github-install-required-notification!
[error retry-fn]
(let [message (start-session-error-message error)
install-url (start-session-install-url error)
can-retry? (fn? retry-fn)]
(notification/show!
[:div.space-y-2
[:div.whitespace-pre-line message]
[:div.flex.flex-wrap.gap-2
(when (string? install-url)
(shui/button
{:size :sm
:on-click (fn []
(open-external-url! install-url))}
"Install GitHub App"))
(when can-retry?
(shui/button
{:variant :outline
:size :sm
:on-click (fn []
(retry-fn))}
"retry"))]]
:warning false github-install-required-notification-uid)))
(defn- normalize-branches
[branches]
(->> branches
@@ -536,32 +586,42 @@
(do
(notification/show! "Invalid agent session payload." :error false)
nil)
(p/let [resp (db-sync/fetch-json (str base "/sessions")
{:method "POST"
:headers {"content-type" "application/json"}
:body (js/JSON.stringify (clj->js body))}
{:response-schema :sessions/create})
session-id (:session-id resp)
status (:status resp)
stream-url (:stream-url resp)
block-uuid (:block/uuid block)
_ (when-let [raw-message (message-body (:content raw-body))]
(let [coerced (coerce-http-request :sessions/message raw-message)
msg-body (if (map? coerced) coerced raw-message)]
(db-sync/fetch-json (str base "/sessions/" session-id "/messages")
{:method "POST"
:headers {"content-type" "application/json"}
:body (js/JSON.stringify (clj->js msg-body))}
{:response-schema :sessions/message})))]
(update-session-state! block-uuid {:session-id session-id
:status status
:runtime-provider (:runtime-provider resp)
:terminal-enabled (true? (:terminal-enabled resp))
:stream-url stream-url
:started-at (util/time-ms)})
(mark-task-session-created! block-uuid)
(<connect-session-stream! block-uuid stream-url)
resp)))))))
(-> (p/let [resp (db-sync/fetch-json (str base "/sessions")
{:method "POST"
:headers {"content-type" "application/json"}
:body (js/JSON.stringify (clj->js body))}
{:response-schema :sessions/create})
session-id (:session-id resp)
status (:status resp)
stream-url (:stream-url resp)
block-uuid (:block/uuid block)
_ (when-let [raw-message (message-body (:content raw-body))]
(let [coerced (coerce-http-request :sessions/message raw-message)
msg-body (if (map? coerced) coerced raw-message)]
(db-sync/fetch-json (str base "/sessions/" session-id "/messages")
{:method "POST"
:headers {"content-type" "application/json"}
:body (js/JSON.stringify (clj->js msg-body))}
{:response-schema :sessions/message})))]
(notification/clear! github-install-required-notification-uid)
(update-session-state! block-uuid {:session-id session-id
:status status
:runtime-provider (:runtime-provider resp)
:terminal-enabled (true? (:terminal-enabled resp))
:stream-url stream-url
:started-at (util/time-ms)})
(mark-task-session-created! block-uuid)
(<connect-session-stream! block-uuid stream-url)
resp)
(p/catch (fn [error]
(if (= 412 (:status (ex-data error)))
(show-github-install-required-notification!
error
(fn []
(-> (<start-session! block opts)
(p/catch (fn [_] nil)))))
(notification/show! (start-session-error-message error) :error false))
nil)))))))))
(defn- publish-request-body
[{:keys [title body commit-message head-branch base-branch create-pr? force?]}]