DEV Community

Cover image for Connect Jenkins to GitHub: Deploy Keys, PAT and Webhooks
Dinesh Wijethunga
Dinesh Wijethunga

Posted on Originally published at dineshstack.com

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.


Originally published at dineshstack.com — read the full version with code samples and updates there.

Top comments (0)