Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 94 additions & 0 deletions .github/workflows/linear.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
name: Create Linear ticket on review assignment

on:
pull_request:
types:
- assigned
- review_requested
- review_request_removed

jobs:
create-linear-issue:
if: github.event.action == 'assigned' || github.event.action == 'review_requested'
runs-on: ubuntu-latest

steps:
- name: Extract PR info
id: pr
run: |
echo "title=${{ github.event.pull_request.title }}" >> $GITHUB_OUTPUT
Copy link

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)

Fix in Cursor Fix in Web

Copy link

Copilot AI Dec 10, 2025

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
Suggested change
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
}

Copilot uses AI. Check for mistakes.
echo "number=${{ github.event.pull_request.number }}" >> $GITHUB_OUTPUT
echo "url=${{ github.event.pull_request.html_url }}" >> $GITHUB_OUTPUT
echo "repo=${{ github.repository }}" >> $GITHUB_OUTPUT

- name: Create Linear issue
id: linear
env:
LINEAR_API_KEY: ${{ secrets.LINEAR_API_KEY }}
LINEAR_TEAM_ID: ${{ secrets.LINEAR_TEAM_ID }}
LINEAR_PROJECT_ID: ${{ secrets.LINEAR_PROJECT_ID }}
GITHUB_ASSIGNEE: ${{ github.event.assignee.login }}
GITHUB_REVIEWER: ${{ github.event.requested_reviewer.login }}
run: |
Copy link

Copilot AI Dec 10, 2025

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.

Suggested change
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

Copilot uses AI. Check for mistakes.
# 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" ;;
Comment on lines +38 to +41
Copy link

Copilot AI Dec 10, 2025

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.

Suggested change
"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

Copilot uses AI. Check for mistakes.
*) LINEAR_ASSIGNEE_ID="" ;;
esac

Comment on lines +32 to +44
Copy link

Copilot AI Dec 10, 2025

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.

Suggested change
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

Copilot uses AI. Check for mistakes.
TITLE="Review PR: ${{ steps.pr.outputs.title }}"
DESCRIPTION="GitHub PR: ${{ steps.pr.outputs.url }}\nRepository: ${{ steps.pr.outputs.repo }}\nPR #${{ steps.pr.outputs.number }}"
Copy link

Copilot AI Dec 10, 2025

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: ..."

Suggested change
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 }}'

Copilot uses AI. Check for mistakes.

QUERY='mutation IssueCreate($input: IssueCreateInput!) {
issueCreate(input: $input) {
issue {
id
identifier
url
}
}
}'

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,
Copy link

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)

Fix in Cursor Fix in Web

variables: {
input: {
title: $title,
description: $desc,
teamId: $teamId,
projectId: ($projId | select(. != "")),
assigneeId: ($assigneeId | select(. != ""))
}
}
}'
)
Comment on lines +58 to +76
Copy link

Copilot AI Dec 10, 2025

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.

Copilot uses AI. Check for mistakes.

RESPONSE=$(curl -s \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $LINEAR_API_KEY" \
-d "$INPUT" \
https://api.linear.app/graphql)

Copy link

Copilot AI Dec 10, 2025

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.

Suggested change
# 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

Copilot uses AI. Check for mistakes.
echo "response=$RESPONSE" >> $GITHUB_OUTPUT

- name: Comment on PR with Linear issue
if: always()
Copy link

Copilot AI Dec 10, 2025

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.

Suggested change
if: always()
if: steps.linear.outcome == 'success'

Copilot uses AI. Check for mistakes.
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
ISSUE_URL=$(echo '${{ steps.linear.outputs.response }}' | jq -r '.data.issueCreate.issue.url // empty')
Copy link

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.

Fix in Cursor Fix in Web

if [ -n "$ISSUE_URL" ]; then
gh pr comment ${{ steps.pr.outputs.number }} --body "Created Linear issue: $ISSUE_URL"
Copy link

Copilot AI Dec 10, 2025

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.

Suggested change
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"

Copilot uses AI. Check for mistakes.
fi
Loading