Skip to content

Linear GitHub Worker

Moves PEA issues from Approved to Live once every PR linked to them (the issue's "Diffs") has reached main. QA approves issues by hand, often before the release reaches main and sometimes after, so the Worker checks all Approved issues on a schedule instead of listening for a single event.

Schedule

Cloudflare cron triggers run in UTC, so the times in wrangler.toml are shifted from IST (UTC+5:30):

Cron (UTC) IST
30 0 * * * 06:00, every day
30 12 * * * 18:00, every day

When an issue moves

For each Approved PEA issue, the Worker takes its attachments that are GitHub PR URLs.

The issue's PRs Result
None Stays Approved (no linked PRs)
Any PR outside the Scenarix org Stays Approved: the GitHub App can't read it
Any PR open, including drafts Stays Approved
A PR closed without merging Ignored, because it was abandoned
A merged PR whose merge commit isn't on main Stays Approved until the release lands
Only abandoned PRs Stays Approved (no merged PRs)
Every merged PR is on main Moves to Live
A PR or repo can't be read (deleted PR, or a repo the app isn't installed on) Logged as failed, and the run errors

A merge commit is on main when GitHub's compare of main against it finds nothing main lacks (aheadBy === 0). This holds whether the PR merged into staging and shipped with a release, or merged into main directly. A move to Live is a normal Linear status change, so feedback-worker still sends the reporter its "resolved" email for Studio Feedback issues.

Requests per run

  • Linear:
  • 1 request for the team's states.
  • 1 request per 50 Approved issues.
  • 1 request per move.
  • GitHub: 2 requests to get an installation token, then 2 GraphQL requests covering every PR in every repo. One fetches PR states and merge commits; the other compares those commits to main. The GraphQL requests split again past 50 PRs or commits.

A typical run stays well under the Free plan's 50-subrequest limit. If a large release ever goes past it, the moves that were left over are logged as failed. The issues are still Approved, so the next run moves them.

Configuration

Name Kind Purpose
DRY_RUN Secret, set by hand Only "false" moves issues. Unset or any other value logs would_move instead. CI never touches it
LINEAR_AUTO_PR_API_KEY Secret Synced from the repo secret of the same name: the "Auto PR" app token (read + write). The deploy fails without it
AUTO_PR_BOT_APP_ID Secret Synced from the repo variable of the same name
AUTO_PR_BOT_APP_PRIVATE_KEY Secret Synced from the repo secret of the same name
GITHUB_TOKEN Local only A personal token used in place of the app, so a local run doesn't need the app's key. CI never sets it

deploy-linear-github-worker.yml runs the tests, syncs the secrets and deploys on every push to main that touches this directory or the shared helpers it imports from worker/src/lib.

Turning moves on and off

DRY_RUN is a secret that CI doesn't sync, so a deploy never resets it, and a change applies from the next run without a redeploy. A fresh deploy has it unset, which means dry run.

  • Dashboard: Workers & Pages → linear-github-worker → Settings → Variables and Secrets → DRY_RUN. Set it to false to move issues, or true to only log.
  • CLI:
    cd linear-github-worker
    printf false | npx wrangler secret put DRY_RUN   # start moving issues
    printf true  | npx wrangler secret put DRY_RUN   # back to logging only
    

The sweep_complete log line's dryRun field shows which mode each run used.

Logs

Each run writes one JSON line per issue, then a summary line. View them in Workers Logs.

event Fields
moved / would_move issue, title, url, pulls
skipped issue, reason
failed issue, url, error
sweep_complete cron, dryRun, checked, moved (or wouldMove), failed, skipped

Local run

cd linear-github-worker
npm ci
npm test
cp .dev.vars.example .dev.vars   # then fill in the secrets
npm run dev                      # wrangler dev --test-scheduled
curl "http://localhost:8787/cdn-cgi/local/scheduled?cron=30+0+*+*+*"

The run's log lines appear in the npm run dev terminal. Keep DRY_RUN=true in .dev.vars: a real move from a local run emails the reporter the same way a deployed one does.

If you don't have the app's private key, set GITHUB_TOKEN instead. Use a fine-grained token owned by Scenarix, with access to the repos and read-only Contents and Pull requests.