Linear CI Integration
Overview
Integrate Linear into GitHub Actions CI/CD pipelines: run integration tests against the Linear API, automatically link PRs to issues, transition issue states on PR events, and create Linear issues from build failures.
Prerequisites
- GitHub repository with Actions enabled
- Linear API key stored as GitHub secret
- npm/pnpm project with
@linear/sdk configured
Instructions
Step 1: Store Secrets in GitHub
gh secret set LINEAR_API_KEY --body "lin_api_xxxxxxxxxxxx"
gh secret set LINEAR_WEBHOOK_SECRET --body "whsec_xxxxxxxxxxxx"
gh variable set LINEAR_TEAM_ID --body "team-uuid-here"
Step 2: Integration Test Workflow
name: Linear Integration Tests
on:
push:
branches: [main]
pull_request:
env:
LINEAR_API_KEY: ${{ secrets.LINEAR_API_KEY }}
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm run test:linear
env:
LINEAR_API_KEY: ${{ secrets.LINEAR_API_KEY }}
- uses: actions/upload-artifact@v4
if: always()
with:
name: test-results
path: test-results/
Step 3: Integration Test Suite
import { describe, it, expect, beforeAll, afterAll } from "vitest";
import { LinearClient } from "@linear/sdk";
describe("Linear Integration", () => {
let client: LinearClient;
let teamId: string;
const cleanup: string[] = [];
beforeAll(async () => {
const apiKey = process.env.LINEAR_API_KEY;
if (!apiKey) throw new Error("LINEAR_API_KEY required for integration tests");
client = new LinearClient({ apiKey });
const teams = await client.teams();
teamId = teams.nodes[0].id;
});
afterAll(async () => {
for (const id of cleanup) {
try { await client.deleteIssue(id); } catch {}
}
});
it("authenticates successfully", async () => {
viewer = client.;
(viewer.).();
(viewer.).();
});
(, () => {
result = client.({
teamId,
: ,
: ,
});
(result.).();
issue = result.;
(issue?.).();
(issue) cleanup.(issue.);
});
(, () => {
issues = client.({
: ,
: { : { : { : teamId } } },
});
(issues..).();
});
(, () => {
teams = client.();
states = teams.[].();
(states..).();
(states..( s. === )).();
});
});
Step 4: PR-to-Issue Linking Workflow
Automatically update Linear issues when PRs are opened, merged, or closed. Extracts issue identifiers from branch names (e.g., feature/ENG-123-description).
name: Sync PR to Linear
on:
pull_request:
types: [opened, closed]
jobs:
sync:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- name: Extract Linear issue ID from branch
id: extract
run: |
BRANCH="${{ github.head_ref }}"
ISSUE_ID=$(echo "$BRANCH" | grep -oE '[A-Z]+-[0-9]+' | head -1 || true)
echo "issue_id=$ISSUE_ID" >> $GITHUB_OUTPUT
- name: Update Linear issue
if: steps.extract.outputs.issue_id
env:
LINEAR_API_KEY: ${{
Step 5: PR Sync Script
import { LinearClient } from "@linear/sdk";
import { parseArgs } from "util";
const { values } = parseArgs({
options: {
issue: { type: "string" },
pr: { type: "string" },
action: { type: "string" },
merged: { type: "string" },
},
});
async function main() {
const client = new LinearClient({ apiKey: process.env.LINEAR_API_KEY! });
const results = await client.issueSearch(values.issue!);
const issue = results.nodes[0];
if (!issue) {
console.log(`Issue ${values.issue} not found — skipping`);
return;
}
const prUrl = `https://github.com/${process.env.GITHUB_REPOSITORY}/pull/${values.pr}`;
client.({
: issue.,
: ,
});
team = issue.;
states = team!.();
(values. === ) {
reviewState = states..(
s..().() || s..().()
);
(reviewState) client.(issue., { : reviewState. });
} (values. === && values. === ) {
doneState = states..( s. === );
(doneState) client.(issue., { : doneState. });
}
.();
}
().(.);
Step 6: Create Issue on CI Failure
name: Create Linear Issue on Failure
on:
workflow_run:
workflows: ["CI"]
types: [completed]
jobs:
create-issue:
if: ${{ github.event.workflow_run.conclusion == 'failure' }}
runs-on: ubuntu-latest
steps:
- name: Create Linear issue for build failure
env:
LINEAR_API_KEY: ${{ secrets.LINEAR_API_KEY }}
run: |
curl -s -X POST https://api.linear.app/graphql \
-H "Authorization: $LINEAR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"query": "mutation($input: IssueCreateInput!) { issueCreate(input: $input) { success issue { identifier url } } }",
"variables": {
"input": {
"teamId": "${{ vars.LINEAR_TEAM_ID }}",
"title": "[CI] Build failure: ${{ github.event.workflow_run.head_branch }}",
"description": "Build failed on branch `${{ github.event.workflow_run.head_branch }}`.\n\nView run",
"priority": 1
}
}
}'
Error Handling
| Error | Cause | Solution |
|---|
Secret not found | Missing GitHub secret | Add LINEAR_API_KEY to repo Settings > Secrets > Actions |
| Issue not found | Wrong identifier or workspace | Verify branch naming convention matches team key |
Permission denied | API key lacks write scope | Regenerate key with write access |
| Duplicate CI issues | Failure workflow runs repeatedly | Add deduplication check before creating |
Examples
PR Template for Linear Integration
<!-- .github/PULL_REQUEST_TEMPLATE.md -->
## Linear Issue
<!-- Use magic words: Fixes, Closes, Resolves -->
Fixes ENG-XXX
## Changes
-
## Testing
- [ ] Unit tests pass
- [ ] Integration tests pass
Resources