Skip to main content

GitHub provider connection (hosted TypeScript service)

Keycli supports a narrow but real GitHub Actions live slice in the hosted control plane. Use this after the preview-only Vercel demo if you want to show the flagship mixed-provider wedge.

What is live now

  • ✅ store a GitHub ProviderConnection
  • ✅ test/validate that connection from the hosted API
  • ✅ upsert/rotate a repository Actions secret through the GitHub API
  • ✅ surface workspace-scoped GitHub readiness through authenticated GET /v1/capabilities
  • ✅ participate in mixed-provider execution when every provider in the plan is connected and supported
  • ✅ post a plan summary comment into the linked GitHub issue/PR when a github-comment approval request is created and a connected GitHub ProviderConnection exists
  • ✅ capture approval from a signed GitHub issue_comment webhook when a comment contains an explicit /keycli approve <plan-id> command and matches the plan’s repo/issue-or-PR context + allowlist

What is not built yet

  • ❌ GitHub App installation flow
  • ❌ broad GitHub automation beyond the secret-rotation + approval wedge

Token expectations

Use a GitHub token that can:
  • read the current user
  • read repository metadata for the target repo
  • write Actions secrets for the target repo
In practice that means a fine-grained PAT or equivalent token with at least:
  • Actions: read/write
  • Metadata: read
  • Issues: write (or equivalent issue/PR comment permission for the repos where Keycli should post approval requests)

Configure a GitHub connection

Start the hosted API:
npm run api:hosted
Bootstrap a workspace and capture the owner token:
BOOTSTRAP=$(curl -s -X POST http://localhost:8788/v1/bootstrap \
  -H 'content-type: application/json' \
  -d '{"workspaceName":"github-demo","ownerName":"ricco"}')

export KEYCLI_TOKEN=$(printf '%s' "$BOOTSTRAP" | node -e 'process.stdin.on("data",d=>{const x=JSON.parse(d); console.log(x.token.token)})')
Create the GitHub connection. Recommended: use an env-backed secret ref so Keycli never stores the raw token in the API object. If the token should only be trusted for one repo, bind it explicitly with scope.
export GITHUB_TOKEN=...

curl -s -X POST http://localhost:8788/v1/connections \
  -H "authorization: Bearer $KEYCLI_TOKEN" \
  -H 'content-type: application/json' \
  -d '{
    "provider": "github",
    "label": "main",
    "authMethod": "api_token",
    "credentialRef": "env:GITHUB_TOKEN",
    "scope": {
      "provider": "github",
      "repository": "demo-org/demo-app"
    }
  }'

Test the connection

This validates the token and, if you pass repository, confirms repository access for the live path.
export KEYCLI_GITHUB_CONNECTION_ID=conn_...
export KEYCLI_GITHUB_REPOSITORY=demo-org/demo-app

curl -s -X POST "http://localhost:8788/v1/connections/$KEYCLI_GITHUB_CONNECTION_ID/test" \
  -H "authorization: Bearer $KEYCLI_TOKEN" \
  -H 'content-type: application/json' \
  -d "{\"repository\":\"$KEYCLI_GITHUB_REPOSITORY\"}"
Expected success shape:
{
  "test": {
    "ok": true,
    "status": "connected",
    "liveExecutionAvailable": true,
    "reason": "github_repository_access_confirmed"
  }
}
If the connection is repo-scoped and you test a different repo, Keycli returns reason: "github_scope_mismatch" and keeps live execution unavailable for that target.

Inspect workspace readiness

curl -s http://localhost:8788/v1/capabilities \
  -H "authorization: Bearer $KEYCLI_TOKEN"
Relevant response fragment:
{
  "capabilities": {
    "workspace": {
      "liveExecution": {
        "github-actions": {
          "supported": true,
          "configured": true,
          "available": true,
          "reason": "connected",
          "scope": "github repository demo-org/demo-app",
          "connectionScope": {
            "provider": "github",
            "repository": "demo-org/demo-app"
          }
        }
      }
    }
  }
}

Narrow GitHub-native approval capture

This is intentionally small and honest:
  • you create an approval-gated plan with requestChannel: "github-comment"
  • you attach the GitHub repo + issue/PR context where approval should happen
  • you allowlist the GitHub usernames that may approve
  • if a GitHub ProviderConnection is connected, Keycli posts a summary comment back into that same thread with the exact approval command
  • GitHub sends an issue_comment webhook to Keycli
  • Keycli approves the existing plan only if the comment body contains the exact command shape /keycli approve <plan-id> and the repo/context/approver all match
Set the shared webhook secret on the API host:
export KEYCLI_GITHUB_WEBHOOK_SECRET=replace_me
npm run api:hosted
Create an approval-gated plan tied to a PR or issue:
PLAN=$(curl -s -X POST http://localhost:8788/v1/plans \
  -H "authorization: Bearer $KEYCLI_AGENT_TOKEN" \
  -H 'content-type: application/json' \
  -d '{
    "action": {
      "action": "rotate_secret",
      "secretName": "STRIPE_SECRET",
      "targets": [
        { "provider": "github-actions", "repository": "demo-org/demo-app" }
      ]
    },
    "requestChannel": "github-comment",
    "approval": {
      "github": {
        "repository": "demo-org/demo-app",
        "issueNumber": 42,
        "issueKind": "pull_request",
        "allowedApprovers": ["ricco"]
      }
    }
  }')

export PLAN_ID=$(printf '%s' "$PLAN" | node -e 'process.stdin.on("data",d=>{const x=JSON.parse(d); console.log(x.plan.id)})')
printf '%s\n' "$PLAN"
If a connected GitHub ProviderConnection exists, Keycli immediately posts a plan summary comment into that issue/PR thread. The plan response includes the exact command and the delivery status:
{
  "plan": {
    "approval": {
      "github": {
        "command": "/keycli approve plan_...",
        "requestComment": {
          "status": "posted",
          "commentId": "1234567890",
          "commentUrl": "https://github.com/demo-org/demo-app/pull/42#issuecomment-1234567890"
        }
      }
    }
  }
}
If no connected GitHub ProviderConnection exists, Keycli still creates the plan but records requestComment.status: "failed" so the missing posting step is explicit in plan metadata and audit logs. Configure a GitHub webhook for that repo to point at:
POST /v1/workspaces/<workspace-id>/github/webhooks/comments
Use the same KEYCLI_GITHUB_WEBHOOK_SECRET value as the GitHub webhook secret. Only the Issue comments event is needed. What this slice does not do yet:
  • it does not install a GitHub App for you
  • it does not retry or reconcile GitHub comment delivery beyond the single post attempt recorded in plan metadata
  • it does not infer approvers from repo membership; the allowlist on the plan is the source of truth tonight

Flagship live wedge: Vercel + GitHub Actions

This is the strongest live story today, but it is not the safest first demo. Prereqs:
  • Vercel connection is connected
  • GitHub connection is connected
  • you know the Vercel project and GitHub repo you want to target
Create an agent token:
AGENT=$(curl -s -X POST http://localhost:8788/v1/tokens \
  -H "authorization: Bearer $KEYCLI_TOKEN" \
  -H 'content-type: application/json' \
  -d '{"principalKind":"agent","principalName":"codex","label":"mixed-live-demo"}')

export KEYCLI_AGENT_TOKEN=$(printf '%s' "$AGENT" | node -e 'process.stdin.on("data",d=>{const x=JSON.parse(d); console.log(x.token)})')
Create the plan:
PLAN=$(curl -s -X POST http://localhost:8788/v1/plans \
  -H "authorization: Bearer $KEYCLI_AGENT_TOKEN" \
  -H 'content-type: application/json' \
  -H 'idempotency-key: mixed-live-demo-1' \
  -d "{
    \"action\": {
      \"action\": \"rotate_secret\",
      \"secretName\": \"STRIPE_SECRET\",
      \"targets\": [
        { \"provider\": \"vercel\", \"project\": \"$KEYCLI_VERCEL_PROJECT\", \"environment\": \"prod\" },
        { \"provider\": \"github-actions\", \"repository\": \"$KEYCLI_GITHUB_REPOSITORY\" }
      ],
      \"deployAfter\": true
    },
    \"value\": \"rotated_demo_value\"
  }")

export PLAN_ID=$(printf '%s' "$PLAN" | node -e 'process.stdin.on("data",d=>{const x=JSON.parse(d); console.log(x.plan.id)})')
printf '%s\n' "$PLAN"
Expected properties:
  • plan.execution.mode: "provider-api"
  • plan.execution.reason: "all_provider_connections_ready"
  • plan.risk.level: "high"
  • response nextAction: "wait_for_approval"
Approve it with the owner token, then apply it with the agent token. At that point Keycli will:
  1. rotate the Vercel env var via Vercel API
  2. rotate the GitHub Actions secret via GitHub API
  3. trigger the Vercel deploy step
  4. persist audit + run events without leaking the secret value