GitHub Projects Sync Wizard guide
A complete guide to setting up two-way real-time sync between GitHub Projects Status and issue labels.
Contents
Overview
The problem
GitHub Actions does not support the projects_v2_item event as a trigger.
# ❌ this does not work
on:
projects_v2_item:
types: [edited]So when you change a Status on the Projects board, the issue label is not synced automatically.
The solution
A Cloudflare Worker receives the GitHub organization webhook directly and handles it.
| Sync direction | Handled by | Trigger |
|---|---|---|
| Label → Status | GitHub Actions | Issue label change |
| Status → Label | Cloudflare Worker | Projects Status change |
Architecture
Overall flow
┌─────────────────────────────────────────────────────────────────────┐
│ GitHub Projects Board │
│ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌──────────┐ ┌───────┐│
│ │ 작업전 │ → │ 작업중 │ → │ 피드백 │ → │담당자확인│ → │작업완료││
│ └─────────┘ └─────────┘ └─────────┘ └──────────┘ └───────┘│
│ │ │
│ │ drag a card (Status change) │
└──────────────────────┼──────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────┐
│ GitHub Organization Webhook │
│ Event: projects_v2_item │
│ Action: edited │
└──────────────────────────────────────────────────────────────────────┘
│
│ HTTPS POST (with X-Hub-Signature-256)
▼
┌──────────────────────────────────────────────────────────────────────┐
│ Cloudflare Worker (Edge) │
│ │
│ Processing order: │
│ 1. Verify the webhook secret (HMAC-SHA256) │
│ 2. Filter events (only projects_v2_item + edited) │
│ 3. Read the current Status with the GraphQL API │
│ 4. Sync the issue label with the REST API │
└──────────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────┐
│ GitHub Issue │
│ Labels: [enhancement] [작업 중] ← changed automatically! │
└──────────────────────────────────────────────────────────────────────┘The board columns above use the default Korean labels (작업전 = status: todo, 작업중 = status: in progress, 피드백 = status: feedback, 담당자확인 = status: needs review, 작업완료 = status: done).
Two-way sync
┌─────────────┐ ┌─────────────┐
│ Issue Label │ ◀───────────────────────▶ │ Projects │
│ │ always in sync │ Status │
└──────┬──────┘ └──────┬──────┘
│ │
│ on label change │ on Status change
▼ ▼
┌─────────────┐ ┌─────────────┐
│ GitHub │ │ Cloudflare │
│ Actions │ │ Worker │
└─────────────┘ └─────────────┘Using the wizard (4 simplified steps)
Step 1: Enter the information in the wizard
Open .github/util/common/projects-sync-wizard/projects-sync-wizard.html in a browser, then:
Enter the GitHub Projects URL:
https://github.com/orgs/YOUR-ORG/projects/1When you enter the URL, the organization name and project number are extracted automatically.
Set the worker name (default:
github-projects-sync-worker)- If the name collides during deploy, the script lets you enter a new one.
Check or customize the status labels (defaults come from issue-labels.yml)
- 작업전, 작업중, 담당자확인, 피드백, 작업완료, 보류, 취소
- Important: the label names must exactly match the Status column names in GitHub Projects. If your repository uses the English standard labels (
status: todo,status: in progress,status: needs review,status: feedback,status: done,status: on hold,status: cancelled), enter those names here and use them as the column names.
Webhook secret (generated automatically)
Click "Worker 파일 ZIP 다운로드" (download the worker files as ZIP)
Step 2: Run the script once
# unzip and move into the folder
cd github-projects-sync-worker
# Mac/Linux
./projects-sync-worker-setup.sh
# Windows PowerShell
.\projects-sync-worker-setup.ps1What the script does automatically:
- Installs npm dependencies (handles SSL errors automatically)
- Logs in to Cloudflare (opens the browser automatically)
- Deploys the worker (you can enter a new name if it collides)
- Asks for the GITHUB_TOKEN
- Sets WEBHOOK_SECRET automatically
- Prints the worker URL
Step 3: Set up the GitHub webhook by hand
- Go to Organization Settings → Webhooks
https://github.com/organizations/YOUR-ORG/settings/hooks - Click "Add webhook"
- Enter the settings:
- Payload URL: the worker URL printed by the script
- Content type:
application/json - Secret: the webhookSecret value in config.json
- Events: "Let me select individual events" → check "Project v2 items"
- Click "Add webhook"
Step 4: Test
- On the GitHub Projects board, move an issue card to another Status column
- On the issue page, check that the label changed automatically
- If something goes wrong, check the worker log:
npx wrangler tail
Manual setup
How to set everything up without the wizard.
Cloudflare Worker manual setup
1. Create wrangler.toml
name = "github-projects-sync-worker"
main = "src/index.ts"
compatibility_date = "2024-01-01"
[vars]
PROJECT_NUMBER = "1"
STATUS_FIELD = "Status"
STATUS_LABELS = '["작업전","작업중","담당자확인","피드백","작업완료","보류","취소"]'
ORG_NAME = "YOUR-ORG"2. Set the secrets
npx wrangler secret put GITHUB_TOKEN
# enter a GitHub PAT (repo, project scopes)
npx wrangler secret put WEBHOOK_SECRET
# enter any secret key (for example: openssl rand -hex 32)GitHub Actions workflow
Edit the environment variables in PROJECT-COMMON-PROJECTS-SYNC-MANAGER.yaml:
env:
STATUS_LABELS: '["작업전", "작업중", "담당자확인", "피드백", "작업완료", "보류", "취소"]'
PROJECT_NUMBER: '1'
ORG_NAME: 'YOUR-ORG'Troubleshooting
SSL error (UNABLE_TO_GET_ISSUER_CERT_LOCALLY)
Cause: a company or school network proxy, or security software
Fix:
# for npm install
npm config set strict-ssl false
npm install
npm config set strict-ssl true
# for wrangler commands
export NODE_TLS_REJECT_UNAUTHORIZED=0 # Mac/Linux
$env:NODE_TLS_REJECT_UNAUTHORIZED=0 # Windows PowerShellCloudflare email verification error
Error: You need to verify your email address to use Workers. [code: 10034]
Fix:
- Open the Cloudflare dashboard
- Check the email verification banner at the top
- Open the email and click the verification link
Subdomain conflict
Error: Subdomain is unavailable, please try a different subdomain
Fix: use a different subdomain name (for example my-org-sync, project-sync)
Webhook 401 error
Cause: the webhook secret does not match
Fix:
- Check the secret in the GitHub webhook settings
- Check that it is the same as WEBHOOK_SECRET in the Cloudflare Worker
- If they differ, reset both to the same value
npx wrangler secret put WEBHOOK_SECRETLabels do not sync
Checklist:
- Check the worker log:
npx wrangler tail - Check webhook deliveries: Organization Settings → Webhooks → Recent Deliveries
- Check that the issue is connected to the project
- Check that the label exactly matches the project's Status column name
Maintenance
When the GitHub PAT expires
cd github-projects-sync-worker
export NODE_TLS_REJECT_UNAUTHORIZED=0
npx wrangler secret put GITHUB_TOKEN
# enter the new tokenWhen you add or change status labels
- Edit
STATUS_LABELSinwrangler.toml - Redeploy the worker
npx wrangler deploy- Make the same change to
STATUS_LABELSin the GitHub Actions workflow
Updating the worker code
cd github-projects-sync-worker
export NODE_TLS_REJECT_UNAUTHORIZED=0
npx wrangler deployChecking the worker log
npx wrangler tailFAQ
Q: Does it cost anything?
A: It is completely free on the Cloudflare Workers free tier.
- 100,000 requests per day
- Typical usage is under 100
Q: Can I use it with a personal repository instead of an organization?
A: No. GitHub Projects V2 supports webhooks only at the organization level. Projects in personal repositories cannot be synced this way.
Q: Can I use it with several projects?
A: Deploy a separate worker for each project, or extend the worker code to handle several projects.
Q: Can it cause an infinite loop?
A: No. The worker code includes infinite loop prevention. If the same label is already there, it skips the update.
Q: Does it apply to PRs too?
A: Yes. The worker code supports both issues and pull requests.
Related files
| File | Description |
|---|---|
.github/util/common/projects-sync-wizard/ | Wizard UI folder |
.github/workflows/PROJECT-COMMON-PROJECTS-SYNC-MANAGER.yaml | Label → Status sync |
Version history
| Version | Date | Changes |
|---|---|---|
| 2.0.0 | 2026-01-21 | Simplified to 4 steps, added a one-click setup script |
| 1.0.0 | 2026-01-21 | Initial release |