Connect Jenkins to GitHub: Deploy Keys, PAT and Webhooks

In this tutorial, I will show you step by step how to connect Jenkins to a private GitHub repository with least-privilege credentials — a per-repo deploy key, a read-only fine-grained PAT, and a push webhook — including the two mistakes that each cost a failed build in a real setup.
The three credentials and their jobs
| Credential | Owner | Used for |
|---|---|---|
| SSH deploy key | deploy user on the server | git pull during deploys |
| Fine-grained PAT | Jenkins | checkout + GitHub API (scan, statuses) |
| Webhook | GitHub → Jenkins | instant builds on push |
Step 1 — Per-repo deploy key (a convention worth stealing)
One key per repo, named accordingly, with an SSH host alias so multiple repo keys coexist on one server:
# as the deploy user on the server
ssh-keygen -t ed25519 -f ~/.ssh/id_repo_myapp -N "" -C "deploy@myapp"
cat >> ~/.ssh/config <<'EOF'
Host github-repo-myapp
HostName github.com
User git
IdentityFile ~/.ssh/id_repo_myapp
IdentitiesOnly yes
EOF
Add the public key in GitHub → repo → Settings → Deploy keys (read-only), then verify and clone through the alias:
ssh -T git@github-repo-myapp # "successfully authenticated"
git clone github-repo-myapp:acme/myapp.git
Step 2 — The fine-grained PAT (minimal, but watch the expiry)
GitHub → Settings → Developer settings → Fine-grained tokens:
- Repository access: Only select repositories → your repo.
- Permissions: Contents: Read-only (Metadata is added automatically). That's all a checkout needs.
- Expiration: the default is 30 days — your pipeline will die silently in a month. Choose Custom and set ~1 year, with a reminder before it lapses.
Store it in Jenkins as Username with password: username = your GitHub username, password = the token, ID = github-token.
Gotcha #1 — credential ID is not the username
We also store the CI database login in Jenkins. First build failed every single test with:
SQLSTATE[HY000] [1045] Access denied for user 'ci-mysql'@'localhost'
ci-mysql was the credential's ID, not the database username — the dialog had been filled with the ID in the username field. When a Jenkinsfile does credentials('ci-mysql'), the ID is only the lookup key; the username/password fields are what get injected. If your logs show your credential ID where a username should be, that's the tell.
Step 3 — The multibranch job, and gotcha #2
New Item → Multibranch Pipeline → Branch Sources → GitHub → credentials github-token, HTTPS repo URL. Save triggers the first scan — which immediately failed:
ERROR: Could not fetch branches from source
"message":"Resource not accessible by personal access token","status":"403"
… Failed to retrieve …/pulls?state=open
The default behaviors include Discover pull requests from origin/forks, which call the PR API — and a Contents-only token can't. Least privilege collided with default assumptions. Two clean fixes:
- No PR workflow (our case): in Behaviors, delete both Discover pull requests entries and set Discover branches → All branches (the default "exclude branches filed as PRs" strategy also touches the PR API).
- PR workflow: add Pull requests: Read-only to the PAT instead.
Rescan: 'Jenkinsfile' found — Scheduled build for branch: master.
Step 4 — The webhook
GitHub repo → Settings → Webhooks → Add webhook:
- Payload URL:
https://jenkins.example.com/github-webhook/(trailing slash matters) - Content type:
application/json - Events: Just the push event
GitHub sends a ping on save — look for the green tick under Recent Deliveries. From now on every push builds within seconds; keep the periodic scan off unless you need branch-deletion cleanup.
Next in the series: post #6 is the Jenkinsfile itself — the full stage graph for a Laravel + React app, the CI MySQL user with its dual-host grant quirk, and why withoutVite() belongs in your base TestCase.
Frequently Asked Questions
Why does my multibranch scan fail with "Resource not accessible by personal access token" 403?
Deploy key or PAT for Jenkins?
How long should a fine-grained PAT live?
Senior Full Stack Developer · Building SaaS products & teaching Laravel/React · 10+ years experience · Founder of Orion360 · Based in Dubai, UAE.
Was this post helpful?



