GitHub sync
Jentrix syncs GitHub Issues two-way with tasks: connect a GitHub App once per workspace, map repositories to boards, and issues stay in lockstep with tasks — title, description, comments, labels, assignees, and open/closed state — in both directions.
GitHub sync is optional, like Pusher/R2/Resend. When the GITHUB_APP_*
environment variables are unset the whole feature no-ops: the settings page shows
"not configured", the webhook receiver returns 200 without doing anything, and
the MCP tools report that GitHub isn't connected.
One-time setup (operator)
Register a GitHub App (GitHub → Settings → Developer settings → GitHub Apps).
<APP_URL> below is your NEXT_PUBLIC_APP_URL (e.g. https://tm.jentrix.ai).
- Homepage URL:
<APP_URL> - Setup URL (Post installation):
<APP_URL>/api/github/setup— and tick "Redirect on update" so adding/removing repos returns to Jentrix. - Webhook → Active: on. Webhook URL:
<APP_URL>/api/github/webhook. - Webhook secret: generate one (
openssl rand -hex 32) and use the same value forGITHUB_APP_WEBHOOK_SECRET. - Repository permissions: Issues — Read & write; Metadata — Read (mandatory,
auto). For the Software Delivery OS (PR + CI observability), also grant the
delivery read scopes: Pull requests — Read; Checks — Read; Actions — Read
(run logs/artifacts); and, optionally, Commit statuses — Read and Deployments —
Read. To enable governed agent writes (open branches, push commits, open
PRs), upgrade two of those to write: Pull requests — Read & write and
Contents — Read & write. This is safe: every repo write goes through a
per-operation, brokered, scoped, short-lived token (one repo, minutes-long
TTL), and the standing/cached sync token is minted without
contents(issue-sync-write + delivery-read only) — so it can never write repository contents even though the App now holds the scope. Writes stay dark (reads/Issue sync unaffected) until an admin re-authorizes. Leave these two at Read-only if you only want observability. - Subscribe to events:
Issues,Issue comment,Label(Issue sync), plus the delivery events. Required for delivery (these gate the "upgrade available" nudge):Pull request,Check run,Check suite,Workflow run. Recommended (useful, not blocking):Pull request review,Status,Workflow job. (Set the matching permission first — the events list only appears once a permission grants them. Installation lifecycle events —installation/installation_repositories— are delivered to the App automatically and need no subscription.) - User authorization (OAuth): not used — leave "Request user authorization during installation" unchecked. Jentrix identifies the installer via its own session, not GitHub OAuth.
- Where it can be installed: "Only on this account" is fine for connecting your own repos; choose "Any account" only for a multi-tenant deployment.
After creating it: generate a private key (.pem), then collect the App
ID and App slug (the <slug> in github.com/apps/<slug>).
Then set the env vars (see .env.example):
- Required:
GITHUB_APP_ID,GITHUB_APP_PRIVATE_KEY(PEM; escape newlines as\non one line),GITHUB_APP_WEBHOOK_SECRET,GITHUB_APP_SLUG(powers the Connect button). Plus the existingAUTH_SECRET(signs the install-callback state) andCRON_SECRET(guards the sync cron). - Optional:
GITHUB_APP_CLIENT_ID/GITHUB_APP_CLIENT_SECRET— reserved for a future GitHub-OAuth identify step; the current flow doesn't use them.
Local dev: GitHub can't reach
localhost. Point the Webhook + Setup URLs at a tunnel (ngrok / cloudflared) forwarding to:3000.
Connecting (workspace admin)
- Workspace settings → Integrations → Connect GitHub. This redirects to
GitHub to install the App on your org/account. The install round-trip is
protected by a signed
state+ a CSRF nonce cookie, and the callback re-checks that you're a workspace admin.- Reusing one GitHub account across workspaces. A GitHub account installs the App only once, but that single installation can serve multiple workspaces. When you open Integrations in another workspace you administer, any GitHub account you've already connected appears as a one-click "Connect <account>" button — no GitHub round-trip needed. (You can still "Install on a new account" to add a different one.)
- Link a repository. Pick a repo the App can access, the target board, the column new (open) issues land in, an optional closed column (otherwise closed issues archive the task), and the sync direction (two-way, GitHub→Jentrix, Jentrix→GitHub, or paused). The mapping is 1:1 — one board ↔ one repo: a repo can sync to only one board (across all workspaces), and a board to only one repo. Repos/boards already linked are hidden from the picker.
- Import existing issues (optional). The "Import" button backfills the repo's current issues into the board — chunked across the sync cron, so large repos import over a few minutes. (Historical comments are not imported; new comments sync going forward.)
Upgrading an existing installation (delivery scopes). Adding the delivery permissions/events above changes the App's requested permissions, so every existing installation must re-consent before GitHub will deliver the new events. Until then the integration degrades gracefully — Issue sync keeps working unchanged, the new events simply aren't sent, and the delivery graph (PR + CI, below) stays empty. The Connections page detects an un-upgraded installation and shows a "GitHub App upgrade available" nudge with a Re-authorize on GitHub button (admins only); re-authorizing grants the read scopes and turns on PR/CI sync. No data is lost by waiting.
What syncs
| GitHub issue | Jentrix task |
|---|---|
| title | title |
| body (Markdown) | description |
| open / closed | column (default / closed column) or archived |
| labels | board labels (created on demand, color carried over) |
| assignees | assignees (mapped GitHub users; unmapped are skipped) |
| comments | comments |
Identity mapping. GitHub users are matched to Jentrix members per workspace
on the Connections page. Unmapped commenters are attributed to the workspace's
GitHub bot user with a **@login on GitHub:** prefix; unmapped assignees are
dropped from the task (we don't auto-invite).
Attribution. Every sync-sourced change runs as a per-workspace GitHub bot
member with source: "github", so the activity feed and comments badge them as
coming from GitHub.
Delivery graph (pull requests + CI)
Once an installation is upgraded (delivery scopes above), Jentrix ingests pull request and CI signals into the delivery graph — durable records, not just status badges. Ingestion is read/observe only: Jentrix records the pipeline; it does not run checks. Opening branches/PRs and pushing commits is possible only through the governed write ops below (and only when the App is granted the write scopes) — never as an inbound side effect.
This section covers the GitHub side of the graph. For the full delivery picture — Vercel deploys, release-control modes, gated merge/deploy, the Control Tower delivery queues, DORA outcomes, releases, incidents, and playbook mining — see the Software Delivery OS guide.
| GitHub event | Effect |
|---|---|
pull_request | Create/update a PR link on the matching task (head/base branch, draft, state, merge state). |
pull_request_review | Record the review decision (approved / changes requested) on the PR. |
check_run | Create/update a child check and roll it up into the CI run. |
check_suite / workflow_run | Create/update the CI run (workflow name, URL, conclusion). |
status | Legacy commit status → a CI run signal. |
PR → task matching. When a PR is seen, Jentrix resolves its owning task
deterministically: a task key (e.g. STK-123) in the head branch name,
then the PR title, then the PR body — and the match only counts if that
task belongs to the PR's linked board (repo ↔ board is 1:1). With no board-valid
match the PR is still recorded as an orphan (no owning task) — never dropped
— and can be adopted onto a task later (in the UI, or over MCP with
link_task_to_pr). A PR's CI conclusion rolls up to a single delivery status on
the owning task; a failing check shows on the task's activity.
Attribution + loop prevention. PR/CI ingestion uses the same source: "github" bot attribution and the same four loop guards as Issue sync (below):
each PR link stores a hash of its last synced state, so a Jentrix-authored PR
update echoing back is recognized and skipped.
Where it shows up. A linked task's detail panel has a Delivery section
listing its pull requests — state (open / draft / merged / closed), review
decision, head → base branch, and a per-CI-run line (workflow name, pass/fail,
failed-check count, evidence count) — each deep-linking out to the PR or the
GitHub Actions run. Board cards carry a compact pill: a PR icon (colored by
state) and a CI icon (passing / running / failing). Both are kept live by three
realtime board events: delivery.pr_linked (a PR was linked to, or changed on,
a task), delivery.check_failed (a CI run entered a failing conclusion on a
task — the high-signal event), and delivery.ci_updated (any other CI change —
success, recovery, re-queue). The pills repaint and an open panel re-fetches its
Delivery section the moment a webhook lands. Orphan PRs (no owning task) carry no
task pill and surface in a later Control Tower queue instead.
CI evidence (logs + reports)
GitHub Actions webhooks only signal that a run finished — they don't carry
the logs. When a workflow_run reaches completed, Jentrix fetches the run's
per-job logs from the Actions API (a second, two-step call), then stores them as
a durable evidence artifact on the run, its pull request, and the owning
task:
- Redacted before storage. Every job log is run through the same secret
redaction (
‹redacted›) used across the platform before anything is written or truncated — GitHub tokens, AWS keys, JWTs, PEM blocks,Bearerheaders, etc. A recognizable secret never reaches storage. Redaction runs on the full text first, so a secret can't survive by straddling a truncation cut. - Size + retention. Logs are byte-capped per job and per document; an
over-cap document is truncated with a
‹…truncated…›notice that deep-links to the full logs on GitHub. Named run artifacts (coverage/JUnit/etc.) are listed as metadata with their names — their downloads are zip blobs (not redactable), so they are referenced, not stored. The stored redacted log is the durable copy and is retained permanently. - One per run (best-effort). A CI run produces a single
LOGartifact, deduped before any fetch; a re-run that maps to the same CI run reuses the first capture. A rare concurrent webhook redelivery could double-store — the cost is a duplicate observability artifact, never corruption. - Provider-optional. The pipeline no-ops cleanly when the GitHub App or R2
(the artifact backend) is unconfigured — like Issue sync, the rest keeps
working. The artifact is reachable from the task's artifacts and carries the
source: "github"attribution.
Governed delivery writes (branches, commits, PRs)
When the App is upgraded with the write scopes (above), Jentrix can perform three repo writes on behalf of an agent or user — each one governed, never a standing capability:
| Op | Action class | What it does |
|---|---|---|
createBranch | repo:branch_create | Branch off a base branch (idempotent — an existing branch is reused). |
pushCommits | repo:push | Push one commit (multiple files, one tree) to a branch. Content-idempotent: if the branch already holds the desired content, nothing is pushed (so a retried turn never double-pushes). CI re-triggers on the new commit — no Actions-write needed. |
openPullRequest | repo:pr_create | Open a PR head → base, stamping the owning task key into the title + body so the echoed pull_request webhook re-owns the PR deterministically (lands MATCHED, not orphan). |
Every write passes through the same four controls, in order:
- Policy — the action class is evaluated by the Policy Engine.
A
repo_push_approval/repo_branch_create_approval/repo_pr_create_approvalpolicy can require admin approval or deny it, with an explainable reason. - Lease — the op runs under a
WorkLeaseon the repo path / PR, so concurrent agents coordinate. - Brokered credential — a per-operation token is minted through the
Credential Broker: scoped to that one repo and to just
the permission the op needs (
contents:writeorpull_requests:write), single-use, and expiring in minutes. The standing sync token is never used for a write (it has nocontentsscope), and no raw long-lived repo token is ever handed to an agent. - Attribution + echo control — the Jentrix-side activity keeps the caller's
attribution (an agent push shows the agent's token badge, not "via GitHub
sync"); the echo of the resulting webhook is suppressed by the stored
lastSyncedHash+OUTBOUNDdirection on the PR link — the loop guards below, not thesourcefield.
A createBranch/pushCommits records a BRANCH/COMMIT evidence artifact on
the task; openPullRequest creates the PR link as MATCHED and fires
delivery.pr_linked. (These ops back the Stage 2.2/2.3 delivery workflow
templates; they are not yet a standalone MCP tool.)
Deploy ingestion (Vercel + generic CI)
The delivery graph spans the deploy step too, fed by inbound webhooks from the
systems that actually deploy. Ingestion is provider-optional (absent config
⇒ a clean no-op). Preview deploys are pure observability with no gate.
Production follows the project's release-control mode: either the merge gate is
the production boundary, or a separate promotion gate invokes Vercel through a
short-lived deploy:production brokered credential.
Vercel (POST /api/vercel/webhook). Add a Jentrix board ↔ Vercel project
mapping (a VercelProjectLink: the Vercel project id, the board, an optional
target → environment map, and the webhook signing secret). Vercel signs each
webhook with HMAC-SHA1 over the raw body (x-vercel-signature); Jentrix
verifies it against that project's secret, dedups the event id, and projects
deployment.created/ready/succeeded/error/canceled/promoted/rollback into a
Deployment record. A deploy is matched to its pull request (and that PR's task)
by commit SHA, scoped to the project's board — so a preview deploy appears on
its PR, and a failed production deploy is recorded against the change that caused
it. Deployment activity is attributed source: "vercel".
Generic CI (POST /api/ci/ingest). Any other CI/deploy system can feed the
same CiRun/Deployment models through a normalized, signed payload. Each
source is provisioned in STACKS_CI_INGEST_SOURCES (a per-source shared secret
plus the workspace/board — and a repo link, if it sends CI runs). The source
signs sha256=HMAC(secret, "<timestamp>.<body>") (its own signed timestamp,
rejected outside a 5-minute replay window), and free-text fields (URLs, names)
are redacted on the way in. Records are attributed source: "ci".
Both paths emit two realtime/outbound events — delivery.deploy_failed (a deploy
entered error/canceled, the high-signal case) and delivery.deploy_updated
(any other state change) — repainting board cards. Failed production deploys
surface in the Control Tower's deploy-failures and rollback-needed queues; the
rollback action is policy-gated, leased, admin-gated, and brokered.
Loop prevention
Two-way sync can't echo itself into a loop. Four guards close it:
- Source tagging — a write that came from GitHub is never pushed back out.
- Content hashing — each link stores a hash of the last synced projection (title, body, state, labels, assignees); an incoming change whose hash matches is skipped. Outbound writes store the hash of GitHub's resulting state, so the webhook GitHub fires for our own edit is a no-op.
- Delivery dedup — GitHub redeliveries (
X-GitHub-Delivery) are dropped. - Last-writer-wins — events older than the last synced state are ignored.
Reconcile + reliability
A cron (/api/cron/github, every 5 min) re-pulls recently-changed issues — and
recently-changed pull requests (open and recently closed) plus the check runs of
open PRs — to catch any missed webhooks, and advances in-progress backfills. The
webhook receiver verifies
X-Hub-Signature-256 (HMAC-SHA256 over the raw body), acknowledges fast, and
processes events after responding.
Because the receiver acknowledges before the post-response CI-evidence capture
runs, a transient Actions/R2 failure would otherwise lose the logs for good. The
same cron therefore re-attempts evidence capture for recently-completed CI runs
that still lack a stored LOG artifact (bounded windowed retry — a run is
retried for a few hours, then aged out; re-capture is idempotent).
Agent / MCP surface
The same operations are available to agents over MCP (see the agent platform reference):
list_github_connections— the workspace's connection + linked-repo count (admin scope).list_github_links— tasks linked to issues, with url/state/last-synced (read scope).link_github_repo/unlink_github_repo— manage repo↔board mappings (admin scope).link_task_to_issue— link an existing task to an issue by number (write scope; idempotent).
Delivery graph (PR + CI)
list_delivery_items— the workspace's PR links with PR state, link state (MATCHED/ORPHAN), owning task key, and aggregate CI signal; filter by board,linkState(ORPHAN = adoptable PRs), or PR state (read scope; paginated).get_pull_request— one PR link in full: review decision, mergeable state, head/base branch, CI runs with check counts, and the redacted CI-evidence count (read scope).list_ci_runs/get_ci_run— CI runs (filter by PR or task) with check roll-up counts; the single-run read adds the individual child checks. GitHub run/suite ids cross as strings (read scope).link_task_to_pr— adopt an orphan PR link onto a task: sets the owner and flipslinkStateto MATCHED (write scope; idempotent,expectedUpdatedAtguard). The task must be on the board linked to the PR's repo (repo ↔ board is 1:1); attribution is the caller's, not the GitHub sync bot's.
AI suggestions
When an issue is first imported and an embeddings provider is configured
(VOYAGE_API_KEY), Jentrix posts an advisory comment on the GitHub issue listing
semantically related existing tasks. It never auto-links — it's a hint. On BUGS
boards, the task panel's "Possible duplicates" section also surfaces similar
tasks. Without a provider key this silently does nothing.