-
Notifications
You must be signed in to change notification settings - Fork 7
Assign Linear tickets to review GitHub PRs in Dev Rel Team #594
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This PR is being reviewed by Cursor Bugbot
Details
Your team is on the Bugbot Free tier. On this plan, Bugbot will review limited PRs each billing cycle for each member of your team.
To receive Bugbot reviews on all of your PRs, visit the Cursor dashboard to activate Pro and start your 14-day free trial.
| - name: Extract PR info | ||
| id: pr | ||
| run: | | ||
| echo "title=${{ github.event.pull_request.title }}" >> $GITHUB_OUTPUT |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bug: Script injection vulnerability via PR title interpolation
The PR title is directly interpolated into the shell command using ${{ github.event.pull_request.title }}. An attacker can craft a malicious PR title containing shell metacharacters (like "; malicious_command #) to execute arbitrary commands in the workflow runner. This is a known GitHub Actions script injection vulnerability. The title value is later used again on lines 45-46, propagating the risk. The safe approach is to pass untrusted input through environment variables rather than direct interpolation.
Additional Locations (1)
| env: | ||
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
| run: | | ||
| ISSUE_URL=$(echo '${{ steps.linear.outputs.response }}' | jq -r '.data.issueCreate.issue.url // empty') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bug: Script injection via API response interpolation
The Linear API response stored in steps.linear.outputs.response is directly interpolated into the shell using ${{ steps.linear.outputs.response }}. If the response contains shell metacharacters or if an attacker can influence the API response, this could lead to command injection. External API responses are untrusted input and need to be passed through environment variables rather than direct interpolation.
| --arg projId "$LINEAR_PROJECT_ID" \ | ||
| --arg assigneeId "$LINEAR_ASSIGNEE_ID" \ | ||
| '{ | ||
| query: $ENV.QUERY, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bug: Shell variable not exported for jq $ENV access
The QUERY variable is defined as a shell variable (lines 48-56) but is accessed via $ENV.QUERY in the jq command. The $ENV object in jq only contains exported environment variables, not local shell variables. Since QUERY is never exported, $ENV.QUERY will be null, resulting in a malformed GraphQL request with no query field. The mutation will fail silently.
Additional Locations (1)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR introduces a GitHub Actions workflow that automatically creates Linear tickets when PRs are assigned or when reviewers are requested. The workflow extracts PR information, maps GitHub usernames to Linear user IDs, creates a Linear issue via GraphQL API, and comments on the PR with the created issue link.
Key Changes:
- Adds automation to create Linear tracking tickets for PR reviews
- Maps GitHub usernames to Linear user IDs for automatic assignment
- Integrates Linear workflow with GitHub PR lifecycle events
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| run: | | ||
| # Pick whoever got assigned / requested | ||
| ASSIGNEE="${GITHUB_ASSIGNEE:-$GITHUB_REVIEWER}" | ||
|
|
||
| # Simple mapping GitHub username -> Linear user ID (adjust to your team) | ||
| case "$ASSIGNEE" in | ||
| "nearestnabors") LINEAR_ASSIGNEE_ID="nearestnabors" ;; | ||
| "torresmateo") LINEAR_ASSIGNEE_ID="mateo" ;; | ||
| "vfanelle") LINEAR_ASSIGNEE_ID="valerie" ;; | ||
| "avoguru") LINEAR_ASSIGNEE_ID="guru" ;; | ||
| *) LINEAR_ASSIGNEE_ID="" ;; | ||
| esac | ||
|
|
Copilot
AI
Dec 10, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a team is requested for review instead of an individual, github.event.requested_reviewer will be null, and github.event.requested_team should be used instead. This workflow doesn't handle team review requests, which will cause the Linear assignee to be empty in those cases. Consider adding logic to handle team review requests or documenting that only individual reviewer assignments are supported.
| run: | | |
| # Pick whoever got assigned / requested | |
| ASSIGNEE="${GITHUB_ASSIGNEE:-$GITHUB_REVIEWER}" | |
| # Simple mapping GitHub username -> Linear user ID (adjust to your team) | |
| case "$ASSIGNEE" in | |
| "nearestnabors") LINEAR_ASSIGNEE_ID="nearestnabors" ;; | |
| "torresmateo") LINEAR_ASSIGNEE_ID="mateo" ;; | |
| "vfanelle") LINEAR_ASSIGNEE_ID="valerie" ;; | |
| "avoguru") LINEAR_ASSIGNEE_ID="guru" ;; | |
| *) LINEAR_ASSIGNEE_ID="" ;; | |
| esac | |
| GITHUB_REVIEW_TEAM: ${{ github.event.requested_team.name }} | |
| run: | | |
| # Pick whoever got assigned / requested | |
| ASSIGNEE="${GITHUB_ASSIGNEE:-$GITHUB_REVIEWER}" | |
| # If no individual assignee, check for team review request | |
| if [ -z "$ASSIGNEE" ] && [ -n "$GITHUB_REVIEW_TEAM" ]; then | |
| # Map team name to Linear user ID (adjust to your team mapping) | |
| case "$GITHUB_REVIEW_TEAM" in | |
| "frontend-team") LINEAR_ASSIGNEE_ID="valerie" ;; # example mapping | |
| "backend-team") LINEAR_ASSIGNEE_ID="mateo" ;; # example mapping | |
| *) LINEAR_ASSIGNEE_ID="" ;; # no mapping, leave unassigned | |
| esac | |
| else | |
| # Simple mapping GitHub username -> Linear user ID (adjust to your team) | |
| case "$ASSIGNEE" in | |
| "nearestnabors") LINEAR_ASSIGNEE_ID="nearestnabors" ;; | |
| "torresmateo") LINEAR_ASSIGNEE_ID="mateo" ;; | |
| "vfanelle") LINEAR_ASSIGNEE_ID="valerie" ;; | |
| "avoguru") LINEAR_ASSIGNEE_ID="guru" ;; | |
| *) LINEAR_ASSIGNEE_ID="" ;; | |
| esac | |
| fi |
| echo "response=$RESPONSE" >> $GITHUB_OUTPUT | ||
|
|
||
| - name: Comment on PR with Linear issue | ||
| if: always() |
Copilot
AI
Dec 10, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The if: always() condition means this step will run even if the Linear issue creation fails or the workflow is cancelled. This could lead to misleading comments on the PR. Consider using if: success() or checking steps.linear.outcome == 'success' to only comment when the Linear issue was actually created successfully.
| if: always() | |
| if: steps.linear.outcome == 'success' |
| LINEAR_PROJECT_ID: ${{ secrets.LINEAR_PROJECT_ID }} | ||
| GITHUB_ASSIGNEE: ${{ github.event.assignee.login }} | ||
| GITHUB_REVIEWER: ${{ github.event.requested_reviewer.login }} | ||
| run: | |
Copilot
AI
Dec 10, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The workflow doesn't validate that required secrets (LINEAR_API_KEY, LINEAR_TEAM_ID) are set before making the API call. If these secrets are missing, the curl request will fail silently, and the response parsing will fail with a cryptic error. Consider adding validation at the start of the script to check for required environment variables and exit with a clear error message if they're missing.
| run: | | |
| run: | | |
| # Validate required secrets | |
| if [ -z "$LINEAR_API_KEY" ]; then | |
| echo "Error: Required secret LINEAR_API_KEY is not set." >&2 | |
| exit 1 | |
| fi | |
| if [ -z "$LINEAR_TEAM_ID" ]; then | |
| echo "Error: Required secret LINEAR_TEAM_ID is not set." >&2 | |
| exit 1 | |
| fi |
| -H "Authorization: Bearer $LINEAR_API_KEY" \ | ||
| -d "$INPUT" \ | ||
| https://api.linear.app/graphql) | ||
|
|
Copilot
AI
Dec 10, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The workflow doesn't check if the Linear API request was successful before saving the response. GraphQL APIs can return errors in the response body with a 200 status code. The response should be validated to check for errors (e.g., .errors field) before proceeding. Without this check, the workflow might appear to succeed even when the Linear issue creation failed.
| # Check for errors in the Linear API response | |
| ERRORS=$(echo "$RESPONSE" | jq '.errors') | |
| if [ "$ERRORS" != "null" ]; then | |
| echo "Linear API returned errors: $ERRORS" | |
| exit 1 | |
| fi |
| - name: Extract PR info | ||
| id: pr | ||
| run: | | ||
| echo "title=${{ github.event.pull_request.title }}" >> $GITHUB_OUTPUT |
Copilot
AI
Dec 10, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The PR title is being passed directly to GITHUB_OUTPUT without proper escaping. If the PR title contains special characters like newlines, %, or \r, it could break the output or cause unexpected behavior. Consider using proper escaping or the multiline string syntax for GitHub Actions outputs:
{
echo "title<<EOF"
echo "${{ github.event.pull_request.title }}"
echo "EOF"
} >> $GITHUB_OUTPUT| echo "title=${{ github.event.pull_request.title }}" >> $GITHUB_OUTPUT | |
| { | |
| echo "title<<EOF" >> $GITHUB_OUTPUT | |
| echo "${{ github.event.pull_request.title }}" >> $GITHUB_OUTPUT | |
| echo "EOF" >> $GITHUB_OUTPUT | |
| } |
| esac | ||
|
|
||
| TITLE="Review PR: ${{ steps.pr.outputs.title }}" | ||
| DESCRIPTION="GitHub PR: ${{ steps.pr.outputs.url }}\nRepository: ${{ steps.pr.outputs.repo }}\nPR #${{ steps.pr.outputs.number }}" |
Copilot
AI
Dec 10, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The \n in the DESCRIPTION variable will be treated as literal characters by the shell, not as newlines. To properly include newlines in the description, use $'\n' or actual newline characters in the string. For example: DESCRIPTION="GitHub PR: ${{ steps.pr.outputs.url }}$'\n'Repository: ..."
| DESCRIPTION="GitHub PR: ${{ steps.pr.outputs.url }}\nRepository: ${{ steps.pr.outputs.repo }}\nPR #${{ steps.pr.outputs.number }}" | |
| DESCRIPTION=$'GitHub PR: ${{ steps.pr.outputs.url }}\nRepository: ${{ steps.pr.outputs.repo }}\nPR #${{ steps.pr.outputs.number }}' |
| INPUT=$(jq -n \ | ||
| --arg title "$TITLE" \ | ||
| --arg desc "$DESCRIPTION" \ | ||
| --arg teamId "$LINEAR_TEAM_ID" \ | ||
| --arg projId "$LINEAR_PROJECT_ID" \ | ||
| --arg assigneeId "$LINEAR_ASSIGNEE_ID" \ | ||
| '{ | ||
| query: $ENV.QUERY, | ||
| variables: { | ||
| input: { | ||
| title: $title, | ||
| description: $desc, | ||
| teamId: $teamId, | ||
| projectId: ($projId | select(. != "")), | ||
| assigneeId: ($assigneeId | select(. != "")) | ||
| } | ||
| } | ||
| }' | ||
| ) |
Copilot
AI
Dec 10, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The JSON structure being built is incorrect for a GraphQL request. The query field should be at the root level alongside variables, but this jq command is placing query: $ENV.QUERY inside the JSON (which won't work as $ENV.QUERY is not a valid jq reference). The correct structure should be:
{
"query": "mutation IssueCreate(...) { ... }",
"variables": { "input": { ... } }
}The jq command should be restructured to properly include the QUERY variable at the root level.
| run: | | ||
| ISSUE_URL=$(echo '${{ steps.linear.outputs.response }}' | jq -r '.data.issueCreate.issue.url // empty') | ||
| if [ -n "$ISSUE_URL" ]; then | ||
| gh pr comment ${{ steps.pr.outputs.number }} --body "Created Linear issue: $ISSUE_URL" |
Copilot
AI
Dec 10, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The gh pr comment command is missing the --repo flag. When run in a workflow context without a checked-out repository, the command needs to specify which repository the PR belongs to. Add --repo ${{ github.repository }} to the command.
| gh pr comment ${{ steps.pr.outputs.number }} --body "Created Linear issue: $ISSUE_URL" | |
| gh pr comment ${{ steps.pr.outputs.number }} --repo ${{ github.repository }} --body "Created Linear issue: $ISSUE_URL" |
| "nearestnabors") LINEAR_ASSIGNEE_ID="nearestnabors" ;; | ||
| "torresmateo") LINEAR_ASSIGNEE_ID="mateo" ;; | ||
| "vfanelle") LINEAR_ASSIGNEE_ID="valerie" ;; | ||
| "avoguru") LINEAR_ASSIGNEE_ID="guru" ;; |
Copilot
AI
Dec 10, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Linear assignee IDs in the case statement appear to be usernames rather than actual Linear user IDs. Linear's API requires UUIDs for the assigneeId field (format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx). These string values like "nearestnabors", "mateo", "valerie", and "guru" are likely not valid Linear user IDs and will cause the assignment to fail silently. You need to use the actual Linear user UUIDs, which can be obtained from the Linear API or Linear settings.
| "nearestnabors") LINEAR_ASSIGNEE_ID="nearestnabors" ;; | |
| "torresmateo") LINEAR_ASSIGNEE_ID="mateo" ;; | |
| "vfanelle") LINEAR_ASSIGNEE_ID="valerie" ;; | |
| "avoguru") LINEAR_ASSIGNEE_ID="guru" ;; | |
| # Replace the UUIDs below with the actual Linear user IDs for each assignee | |
| "nearestnabors") LINEAR_ASSIGNEE_ID="11111111-1111-1111-1111-111111111111" ;; # TODO: Replace with actual UUID | |
| "torresmateo") LINEAR_ASSIGNEE_ID="22222222-2222-2222-2222-222222222222" ;; # TODO: Replace with actual UUID | |
| "vfanelle") LINEAR_ASSIGNEE_ID="33333333-3333-3333-3333-333333333333" ;; # TODO: Replace with actual UUID | |
| "avoguru") LINEAR_ASSIGNEE_ID="44444444-4444-4444-4444-444444444444" ;; # TODO: Replace with actual UUID |
Note
Adds a GitHub Actions workflow to auto-create a Linear issue when a PR is assigned or review is requested and comments the issue link on the PR.
.github/workflows/linear.yml):pull_requestevents:assigned,review_requested,review_request_removed(job runs only for the first two).Written by Cursor Bugbot for commit cef91e6. This will update automatically on new commits. Configure here.