Skip to content

Refactor Linear API token: pass through stack instead of setting env var#591

Draft
acreeger wants to merge 1 commit intomainfrom
refactor/issue-590__linear-api-token
Draft

Refactor Linear API token: pass through stack instead of setting env var#591
acreeger wants to merge 1 commit intomainfrom
refactor/issue-590__linear-api-token

Conversation

@acreeger
Copy link
Collaborator

Fixes #590

Refactor Linear API token: pass through stack instead of setting env var

Problem

LinearService constructor sets process.env.LINEAR_API_TOKEN as a side effect (src/lib/LinearService.ts:50), and all functions in src/utils/linear.ts rely on this env var being set via getLinearApiToken()createLinearClient().

This caused a bug (#590) where il list --children called getLinearChildIssues without first instantiating LinearService, so the token was never in the environment.

Setting env vars as a side effect is fragile — any new code path that calls Linear utility functions without going through LinearService will hit the same UNAUTHORIZED error.

Desired behavior

All functions in src/utils/linear.ts should accept the API token as a parameter (with env var as fallback), passed down through the call stack. LinearService should stop setting process.env.LINEAR_API_TOKEN.

Scope

src/utils/linear.ts — Update all exported functions to accept an optional apiToken parameter and pass it to createLinearClient():

  • fetchLinearIssue
  • createLinearIssue
  • createLinearChildIssue
  • createLinearComment
  • updateLinearIssueState
  • getLinearComment
  • updateLinearComment
  • fetchLinearIssueComments
  • getLinearChildIssues (already done in Refactor Linear API token: pass through stack instead of setting env var #590)
  • createLinearIssueRelation
  • getLinearIssueDependencies
  • deleteLinearIssueRelation
  • fetchLinearIssueList (already accepts apiToken)
  • findLinearIssueRelation

src/lib/LinearService.ts — Stop setting process.env.LINEAR_API_TOKEN. Instead, store the token as an instance property and pass it to each linear.ts function call.

src/utils/image-processor.ts — Check if it reads LINEAR_API_TOKEN from env; if so, update to accept it as a parameter.

Tests — Update LinearService.test.ts to remove env var assertions. Test files that set the env var for test setup are fine.


This PR was created automatically by iloom.

@acreeger
Copy link
Collaborator Author

acreeger commented Feb 11, 2026

Complexity Assessment

Classification: COMPLEX

Metrics:

  • Estimated files affected: 6 (LinearService, linear.ts, image-processor, LinearIssueManagementProvider, list-children, issues command)
  • Estimated lines of code: 400-600 (modifications + new parameters + test changes, including substantial updates to LinearIssueManagementProvider.test.ts and image-processor.test.ts)
  • Breaking changes: No (adds optional parameters)
  • Database migrations: No
  • Cross-cutting changes: YES - API token must be passed through call stack
  • File architecture quality: Poor - linear.ts is 828 LOC with multiple concerns
  • Architectural signals triggered: Cross-cutting parameter flow detected
  • Overall risk level: Moderate

Reasoning: This is a COMPLEX issue due to cross-cutting change requirements. The API token must flow through a deep call stack: LinearService → linear.ts functions (14+ functions) → LinearClient, affecting ~20 production call sites across 6 files (~3 in LinearService.ts, ~15 in LinearIssueManagementProvider.ts, ~1 in list-children.ts). Additionally, linear.ts is a large utility file (828 LOC) that will require coordinated updates to multiple function signatures. Note: issues.ts and list-children.ts already pass apiToken in their relevant paths. LinearIssueManagementProvider may need zero changes since mcp.ts explicitly sets LINEAR_API_TOKEN in the environment before spawning the MCP subprocess, and createLinearClient() falls back to process.env.LINEAR_API_TOKEN. While individual changes appear simple, the coordination requirement across multiple layers, the 828 LOC utility file, and substantial test file updates make this COMPLEX.

@acreeger
Copy link
Collaborator Author

Analysis Phase

  • Fetch issue details and understand scope
  • Research createLinearClient() and getLinearApiToken() flow
  • Analyze all 14+ functions in src/utils/linear.ts
  • Analyze LinearService.ts env var setting and method calls
  • Investigate LinearIssueManagementProvider.ts and MCP subprocess context
  • Investigate image-processor.ts LINEAR_API_TOKEN usage
  • Map all callers of linear.ts functions across the codebase
  • Identify test files requiring updates
  • Document cross-cutting architectural flow
  • Write final analysis comment

Estimated time: ~8 minutes

@acreeger
Copy link
Collaborator Author

acreeger commented Feb 11, 2026

Analysis Phase (Deep Investigation)

  • Research createLinearClient() and getLinearApiToken() flow
  • Analyze all 14+ functions in src/utils/linear.ts for apiToken parameter needs
  • Analyze LinearService.ts env var setting and all method calls
  • Investigate LinearIssueManagementProvider.ts and MCP subprocess context
  • Investigate image-processor.ts LINEAR_API_TOKEN usage
  • Map ALL callers of linear.ts functions across the codebase
  • Identify test files requiring updates
  • Document cross-cutting architectural flow analysis
  • Write final analysis

Executive Summary

This refactor eliminates the fragile process.env.LINEAR_API_TOKEN side effect from LinearService constructor (line 50) by threading an explicit apiToken parameter through all 14 exported functions in src/utils/linear.ts. The core change is straightforward: each function gets an optional apiToken parameter passed to createLinearClient(), and LinearService stores the token as an instance property instead of polluting the environment. Critically, LinearIssueManagementProvider.ts and image-processor.ts do NOT need changes because they run in an MCP subprocess where mcp.ts already sets LINEAR_API_TOKEN in the subprocess environment.

Question Answer
Should LinearIssueManagementProvider.ts be updated to pass apiToken explicitly? No. It runs in an MCP subprocess where mcp.ts:79-82 sets LINEAR_API_TOKEN in env and issue-management-server.ts:56 validates it exists. The env var fallback in createLinearClient() handles this correctly. Adding explicit params would be unnecessary complexity.
Should image-processor.ts be updated to accept apiToken? No. processMarkdownImages() is only called from LinearIssueManagementProvider (MCP subprocess context), where LINEAR_API_TOKEN is in env. The getAuthToken('linear') call at line 225 will continue to work via env var.
Should list-children.ts and issues.ts be updated? No changes needed. list-children.ts:111 already passes apiToken from settings to getLinearChildIssues(). issues.ts:150 already passes apiToken to fetchLinearIssueList(). Both already use the explicit parameter pattern.

Impact Summary:

  • 1 file for significant refactor: src/utils/linear.ts -- add optional apiToken param to 12 functions (2 already have it)
  • 1 file for behavior change: src/lib/LinearService.ts -- stop setting env var, store token as instance prop, pass to 3 call sites
  • 1 test file for update: src/lib/LinearService.test.ts -- replace env var assertions with behavior assertions
  • 0 changes needed: LinearIssueManagementProvider.ts, image-processor.ts, list-children.ts, issues.ts

Complete Technical Reference (click to expand for implementation details)

Problem Space Research

Problem Understanding

LinearService constructor sets process.env.LINEAR_API_TOKEN as a side effect (line 50). Any code path calling linear.ts utility functions without first instantiating LinearService hits UNAUTHORIZED errors. This already caused a bug with il list --children calling getLinearChildIssues directly.

Architectural Context

The codebase has two distinct execution contexts for Linear operations:

  1. Main CLI process: LinearService (via IssueTrackerFactory), IssuesCommand, list-children.ts
  2. MCP subprocess: LinearIssueManagementProvider + image-processor.ts, spawned with LINEAR_API_TOKEN in env by mcp.ts

The refactor only affects context #1. Context #2 has its own env var injection mechanism that works correctly.

Edge Cases Identified

  • No token anywhere: createLinearClient() with no apiToken param and no env var -- getLinearApiToken() correctly throws UNAUTHORIZED error. This behavior is preserved.
  • Token in env but not in config: The apiToken ?? getLinearApiToken() fallback at linear.ts:81 preserves backward compatibility.

Codebase Research Findings

Affected Area: src/utils/linear.ts (832 LOC)

Current token flow:

  • getLinearApiToken() (line 64-73): reads process.env.LINEAR_API_TOKEN, throws if missing
  • createLinearClient(apiToken?) (line 80-83): uses apiToken ?? getLinearApiToken()

Functions needing apiToken parameter added (12 functions -- 2 already have it):

Functions that already accept apiToken:

  • getLinearChildIssues (line 501) -- via options?.apiToken
  • fetchLinearIssueList (line 721) -- via options?.apiToken

Functions that need apiToken added (12):

  • fetchLinearIssue (line 128) -- 1 createLinearClient() call at line 131
  • createLinearIssue (line 178) -- 1 createLinearClient() call at line 186
  • createLinearChildIssue (line 240) -- 1 createLinearClient() call at line 249
  • createLinearComment (line 300) -- 1 createLinearClient() call at line 306
  • updateLinearIssueState (line 347) -- 1 createLinearClient() call at line 353
  • getLinearComment (line 396) -- 1 createLinearClient() call at line 399
  • updateLinearComment (line 428) -- 1 createLinearClient() call at line 434
  • fetchLinearIssueComments (line 464) -- 1 createLinearClient() call at line 467
  • createLinearIssueRelation (line 560) -- 1 createLinearClient() call at line 566
  • getLinearIssueDependencies (line 594) -- 1 createLinearClient() call at line 600
  • deleteLinearIssueRelation (line 684) -- 1 createLinearClient() call at line 687
  • findLinearIssueRelation (line 785) -- 1 createLinearClient() call at line 791

Pattern: All 12 functions follow the same pattern -- add optional apiToken?: string as last parameter, pass to createLinearClient(apiToken).

Affected Area: src/lib/LinearService.ts (222 LOC)

Entry point: IssueTrackerFactory.create() at src/lib/IssueTrackerFactory.ts:54 constructs LinearService with linearConfig including apiToken from settings.

Current behavior (line 48-51):

if (this.config.apiToken) {
  process.env.LINEAR_API_TOKEN = this.config.apiToken
}

Changes needed:

  1. Store apiToken as private instance property (e.g., private apiToken?: string)
  2. Remove process.env.LINEAR_API_TOKEN assignment
  3. Pass this.apiToken to each linear.ts function call:
    • fetchLinearIssue(String(identifier)) at line 101 -- add apiToken param
    • createLinearIssue(title, body, this.config.teamId, labels) at line 166 -- add apiToken param
    • updateLinearIssueState(String(identifier), 'In Progress') at line 192 -- add apiToken param

Files NOT Requiring Changes

src/mcp/LinearIssueManagementProvider.ts (305 LOC, 16 call sites):
Runs in MCP subprocess. mcp.ts:79-82 sets LINEAR_API_TOKEN in the subprocess envVars. issue-management-server.ts:56-59 validates it exists at startup. All 16 calls to linear.ts functions use createLinearClient() which falls back to process.env.LINEAR_API_TOKEN. No change needed.

src/utils/image-processor.ts (line 225):
getAuthToken('linear') returns process.env.LINEAR_API_TOKEN. Only called from processMarkdownImages() which is exclusively used by LinearIssueManagementProvider (MCP subprocess context). No change needed.

src/commands/issues.ts (line 150):
Already passes apiToken to fetchLinearIssueList(): fetchLinearIssueList(teamId, { limit, ...(apiToken ? { apiToken } : {}) }). No change needed.

src/utils/list-children.ts (line 111):
Already passes apiToken to getLinearChildIssues(): getLinearChildIssues(parentIssueNumber, apiToken ? { apiToken } : undefined). No change needed.

Test Files

src/lib/LinearService.test.ts (68 LOC) -- REQUIRES UPDATE:

  • Lines 31-66: 5 tests that assert process.env.LINEAR_API_TOKEN behavior
  • These tests should be replaced with tests verifying apiToken is passed to the mocked linear.ts functions
  • The beforeEach/afterEach env var save/restore (lines 15-29) can be removed

src/mcp/LinearIssueManagementProvider.test.ts (695 LOC) -- NO CHANGE NEEDED:

  • Mocks all linear.ts functions at module level
  • Tests do not assert on apiToken parameters
  • Since the production code won't change, tests don't need updating

src/utils/image-processor.test.ts -- NO CHANGE NEEDED:

  • Tests set process.env.LINEAR_API_TOKEN directly for test setup (lines 566-717)
  • Production code (image-processor.ts) is not changing, so tests remain valid

src/commands/issues.test.ts -- NO CHANGE NEEDED:

  • Already tests the apiToken passing behavior (line 191)
  • Production code not changing

src/utils/list-children.test.ts -- NO CHANGE NEEDED:

  • Mocks getLinearChildIssues at module level
  • Production code not changing

Architectural Flow Analysis

Data Flow: apiToken (after refactor)

Entry Point: IssueTrackerFactory.create() at /src/lib/IssueTrackerFactory.ts:49-54 -- reads settings.issueManagement.linear.apiToken, passes in LinearServiceConfig

Flow Path (Main CLI process):

  1. /src/lib/IssueTrackerFactory.ts:54 -- Constructs LinearService(linearConfig) with apiToken in config
  2. /src/lib/LinearService.ts constructor -- Stores apiToken as instance property (currently sets env var)
  3. /src/lib/LinearService.ts:101,166,192 -- Passes this.apiToken to fetchLinearIssue(), createLinearIssue(), updateLinearIssueState()
  4. /src/utils/linear.ts (each function) -- Passes apiToken to createLinearClient(apiToken)
  5. /src/utils/linear.ts:80-83 -- createLinearClient() uses apiToken ?? getLinearApiToken() (env var fallback)

Flow Path (MCP subprocess -- no changes needed):

  1. /src/utils/mcp.ts:79-82 -- Reads token from settings/env, sets in subprocess envVars.LINEAR_API_TOKEN
  2. /src/mcp/issue-management-server.ts:56 -- Validates LINEAR_API_TOKEN in env
  3. /src/mcp/LinearIssueManagementProvider.ts (16 call sites) -- Calls linear.ts functions without explicit token
  4. /src/utils/linear.ts -- createLinearClient() falls back to process.env.LINEAR_API_TOKEN

Affected Interfaces (ALL that must be updated):

  • LinearServiceConfig at /src/lib/LinearService.ts:21-28 -- Keep apiToken field (already exists), update JSDoc to remove "sets process.env" mention
  • 12 function signatures in /src/utils/linear.ts -- Add optional apiToken?: string parameter

Critical Implementation Note: This is a cross-cutting change but with a narrow blast radius. Only LinearService.ts (3 call sites) needs to start passing the token. The 12 function signature updates in linear.ts are mechanical. LinearIssueManagementProvider.ts (16 call sites) does NOT need updating because the env var fallback in the MCP subprocess context handles it.

Affected Files

  • /src/utils/linear.ts -- Add apiToken?: string param to 12 function signatures, pass to createLinearClient(apiToken)
  • /src/lib/LinearService.ts:26-27 -- Update JSDoc for apiToken field
  • /src/lib/LinearService.ts:48-51 -- Replace env var assignment with instance property storage
  • /src/lib/LinearService.ts:101 -- Pass this.apiToken to fetchLinearIssue()
  • /src/lib/LinearService.ts:166 -- Pass this.apiToken to createLinearIssue()
  • /src/lib/LinearService.ts:192 -- Pass this.apiToken to updateLinearIssueState()
  • /src/lib/LinearService.test.ts:11-67 -- Replace env var assertion tests with apiToken-passing behavior tests

Integration Points

  • IssueTrackerFactory.create() passes apiToken via LinearServiceConfig to LinearService constructor
  • LinearService methods call linear.ts utility functions with explicit apiToken
  • linear.ts createLinearClient(apiToken?) already supports optional token with env var fallback
  • MCP subprocess injects LINEAR_API_TOKEN via mcp.ts env var mechanism (independent path, unchanged)

@acreeger
Copy link
Collaborator Author

acreeger commented Feb 11, 2026

Implementation Plan for Issue #590

Summary

Refactor Linear API token passing to eliminate the fragile process.env.LINEAR_API_TOKEN side effect from LinearService constructor. Instead, thread an explicit apiToken parameter through all 12 functions in src/utils/linear.ts that currently rely on the env var, and have LinearService store the token as an instance property and pass it to each call. The createLinearClient() fallback to env var is preserved for MCP subprocess compatibility.

Questions and Key Decisions

Question Answer Rationale
Parameter style for new apiToken params? Simple apiToken?: string as last param Functions without existing options objects get a simple param. The two functions already using options objects (getLinearChildIssues, fetchLinearIssueList) keep their signatures unchanged.
Store token as separate property or use config? Private apiToken instance property Cleaner separation -- the token is extracted from config in constructor, avoids reaching into this.config.apiToken at each call site
Update LinearIssueManagementProvider.ts? No Runs in MCP subprocess where mcp.ts:79-82 injects LINEAR_API_TOKEN into env. The createLinearClient() env var fallback handles this.
Update image-processor.ts? No Only called from MCP subprocess context where env var is guaranteed

High-Level Execution Phases

  1. Update linear.ts function signatures: Add apiToken?: string parameter to 12 functions and pass to createLinearClient(apiToken)
  2. Refactor LinearService.ts: Store token as instance property, remove env var side effect, pass token to all 3 call sites
  3. Rewrite LinearService.test.ts: Replace env var assertions with behavior-focused tests verifying token is passed through
  4. Build verification: Run pnpm build to confirm TypeScript compiles

Quick Stats

  • 0 files for deletion
  • 3 files to modify (linear.ts, LinearService.ts, LinearService.test.ts)
  • 0 new files to create
  • Dependencies: None
  • Estimated complexity: Simple (mechanical, repetitive changes)

Complete Implementation Guide (click to expand for step-by-step details)

Automated Test Cases to Create

Test File: src/lib/LinearService.test.ts (MODIFY -- full rewrite of constructor tests)

Purpose: Verify LinearService stores apiToken and passes it to linear.ts functions instead of setting env var.

Click to expand complete test structure (30 lines)
describe('LinearService', () => {
  describe('constructor', () => {
    it('should not set process.env.LINEAR_API_TOKEN when apiToken provided')
      // Create service with apiToken
      // Assert process.env.LINEAR_API_TOKEN is NOT set (or unchanged)

    it('should store apiToken for later use in API calls')
      // Create service with apiToken
      // Call fetchIssue (which delegates to fetchLinearIssue)
      // Assert fetchLinearIssue was called with the apiToken as second argument
  })

  describe('fetchIssue', () => {
    it('should pass apiToken to fetchLinearIssue')
      // Create service with apiToken config
      // Mock fetchLinearIssue to return a valid issue
      // Call service.fetchIssue('ENG-123')
      // Assert fetchLinearIssue was called with ('ENG-123', 'the-token')
  })

  describe('createIssue', () => {
    it('should pass apiToken to createLinearIssue')
      // Create service with apiToken and teamId config
      // Mock createLinearIssue to return identifier/url
      // Call service.createIssue(...)
      // Assert createLinearIssue called with (title, body, teamId, labels, 'the-token')
  })

  describe('moveIssueToInProgress', () => {
    it('should pass apiToken to updateLinearIssueState')
      // Create service with apiToken config
      // Call service.moveIssueToInProgress('ENG-123')
      // Assert updateLinearIssueState called with ('ENG-123', 'In Progress', 'the-token')
  })
})

Files to Modify

1. src/utils/linear.ts

Change overview: Add apiToken?: string as last parameter to 12 function signatures and pass it to createLinearClient(apiToken).

Functions to update (each needs two changes -- signature + createLinearClient call):

Function Line (signature) Line (createLinearClient()) Notes
fetchLinearIssue 128 131 Add apiToken?: string after identifier: string
createLinearIssue 178-183 186 Add apiToken?: string after _labels?: string[]
createLinearChildIssue 240-245 249 Add apiToken?: string after _labels?: string[]
createLinearComment 300-302 306 Add apiToken?: string after body: string
updateLinearIssueState 347-349 353 Add apiToken?: string after stateName: string
getLinearComment 396 399 Add apiToken?: string after commentId: string
updateLinearComment 428-430 434 Add apiToken?: string after body: string
fetchLinearIssueComments 464 467 Add apiToken?: string after identifier: string
createLinearIssueRelation 560-562 566 Add apiToken?: string after blockedIssueId: string
getLinearIssueDependencies 594-596 600 Add apiToken?: string after direction param
deleteLinearIssueRelation 684 687 Add apiToken?: string after relationId: string
findLinearIssueRelation 785-787 791 Add apiToken?: string after blockedIdentifier: string

Pattern for each function:

// Before:
export async function fetchLinearIssue(identifier: string): Promise<LinearIssue> {
  // ...
  const client = createLinearClient()

// After:
export async function fetchLinearIssue(identifier: string, apiToken?: string): Promise<LinearIssue> {
  // ...
  const client = createLinearClient(apiToken)

No changes needed for: getLinearChildIssues (line 501, already has options?.apiToken), fetchLinearIssueList (line 721, already has options?.apiToken).

Also update JSDoc @param for each function to document the new apiToken parameter.

2. src/lib/LinearService.ts

Change 1 -- Line 26-27: Update JSDoc for apiToken field.

// Before:
/** Linear API token (lin_api_...). If provided, sets process.env.LINEAR_API_TOKEN */

// After:
/** Linear API token (lin_api_...). If provided, passed to Linear API calls */

Change 2 -- Line 38: Add private instance property.

// Add after line 39 (private prompter):
private apiToken?: string

Change 3 -- Lines 45-51: Replace env var side effect with property storage.

// Before:
this.config = config ?? {}
this.prompter = options?.prompter ?? promptConfirmation

// Set API token from config if provided (follows mcp.ts pattern)
if (this.config.apiToken) {
  process.env.LINEAR_API_TOKEN = this.config.apiToken
}

// After:
this.config = config ?? {}
this.prompter = options?.prompter ?? promptConfirmation
this.apiToken = this.config.apiToken

Change 4 -- Line 101: Pass apiToken to fetchLinearIssue.

// Before:
const linearIssue = await fetchLinearIssue(String(identifier))

// After:
const linearIssue = await fetchLinearIssue(String(identifier), this.apiToken)

Change 5 -- Line 166: Pass apiToken to createLinearIssue.

// Before:
const result = await createLinearIssue(title, body, this.config.teamId, labels)

// After:
const result = await createLinearIssue(title, body, this.config.teamId, labels, this.apiToken)

Change 6 -- Line 192: Pass apiToken to updateLinearIssueState.

// Before:
await updateLinearIssueState(String(identifier), 'In Progress')

// After:
await updateLinearIssueState(String(identifier), 'In Progress', this.apiToken)

3. src/lib/LinearService.test.ts

Full rewrite. Replace all 5 env var assertion tests (lines 11-67) with behavior-focused tests that verify apiToken is passed to mocked linear.ts functions.

Key changes:

  • Remove: beforeEach/afterEach env var save/restore (lines 13-29)
  • Remove: All 5 tests asserting process.env.LINEAR_API_TOKEN value (lines 31-66)
  • Add: Test that constructor does NOT set process.env.LINEAR_API_TOKEN
  • Add: Tests verifying fetchLinearIssue, createLinearIssue, updateLinearIssueState are called with apiToken argument
  • Keep: Existing vi.mock setup (lines 4-9)
Click to expand complete test structure (55 lines)
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { LinearService } from './LinearService.js'
import { fetchLinearIssue, createLinearIssue, updateLinearIssueState } from '../utils/linear.js'

vi.mock('../utils/linear.js', () => ({
  fetchLinearIssue: vi.fn(),
  createLinearIssue: vi.fn(),
  updateLinearIssueState: vi.fn(),
}))

describe('LinearService', () => {
  const testToken = 'lin_api_test_token_123'

  describe('constructor', () => {
    it('should not set process.env.LINEAR_API_TOKEN', () => {
      // Ensure env var is clean
      delete process.env.LINEAR_API_TOKEN
      new LinearService({ apiToken: testToken })
      expect(process.env.LINEAR_API_TOKEN).toBeUndefined()
    })
  })

  describe('fetchIssue', () => {
    it('should pass apiToken to fetchLinearIssue', async () => {
      // mock fetchLinearIssue to return a valid LinearIssue
      vi.mocked(fetchLinearIssue).mockResolvedValue({
        id: 'uuid', identifier: 'ENG-123', title: 'Test',
        url: 'https://linear.app/issue/ENG-123', createdAt: '...', updatedAt: '...',
      })
      const service = new LinearService({ apiToken: testToken })
      await service.fetchIssue('ENG-123')
      expect(fetchLinearIssue).toHaveBeenCalledWith('ENG-123', testToken)
    })
  })

  describe('createIssue', () => {
    it('should pass apiToken to createLinearIssue', async () => {
      vi.mocked(createLinearIssue).mockResolvedValue({
        identifier: 'ENG-456', url: 'https://linear.app/issue/ENG-456',
      })
      const service = new LinearService({ apiToken: testToken, teamId: 'ENG' })
      await service.createIssue('Title', 'Body', undefined, ['bug'])
      expect(createLinearIssue).toHaveBeenCalledWith('Title', 'Body', 'ENG', ['bug'], testToken)
    })
  })

  describe('moveIssueToInProgress', () => {
    it('should pass apiToken to updateLinearIssueState', async () => {
      vi.mocked(updateLinearIssueState).mockResolvedValue(undefined)
      const service = new LinearService({ apiToken: testToken })
      await service.moveIssueToInProgress('ENG-123')
      expect(updateLinearIssueState).toHaveBeenCalledWith('ENG-123', 'In Progress', testToken)
    })
  })
})

Detailed Execution Order

Step 1: Update linear.ts function signatures and LinearService.ts behavior

Files: src/utils/linear.ts, src/lib/LinearService.ts

  1. src/utils/linear.ts: Add apiToken?: string param + pass to createLinearClient(apiToken) for all 12 functions listed in table above -> Verify: each function signature has the new param, each createLinearClient() call passes it
  2. src/utils/linear.ts: Update JSDoc @param blocks for each modified function -> Verify: docs mention apiToken
  3. src/lib/LinearService.ts:26: Update JSDoc comment for apiToken config field -> Verify: no mention of setting env var
  4. src/lib/LinearService.ts:38-39: Add private apiToken?: string property -> Verify: property declared
  5. src/lib/LinearService.ts:45-51: Replace env var side effect with this.apiToken = this.config.apiToken -> Verify: no process.env.LINEAR_API_TOKEN assignment
  6. src/lib/LinearService.ts:101: Pass this.apiToken to fetchLinearIssue -> Verify: second arg is this.apiToken
  7. src/lib/LinearService.ts:166: Pass this.apiToken to createLinearIssue -> Verify: sixth arg is this.apiToken
  8. src/lib/LinearService.ts:192: Pass this.apiToken to updateLinearIssueState -> Verify: third arg is this.apiToken

Step 2: Rewrite tests

Files: src/lib/LinearService.test.ts

  1. Replace all constructor env var tests with behavior tests per test structure above -> Verify: pnpm test src/lib/LinearService.test.ts passes
  2. Add tests for fetchIssue, createIssue, moveIssueToInProgress verifying apiToken passing -> Verify: tests pass

Step 3: Build verification

Files: None (build step)

  1. Run pnpm build -> Verify: TypeScript compiles with no errors
  2. Run pnpm test -> Verify: all tests pass

Execution Plan

  1. Run Step 1 (sequential -- modify linear.ts and LinearService.ts together since LinearService imports from linear.ts and the signatures must match)
  2. Run Step 2 (sequential -- tests depend on Step 1 signature changes being complete)
  3. Run Step 3 (sequential -- build/test verification depends on all code changes)

Dependencies and Configuration

None

@acreeger
Copy link
Collaborator Author

acreeger commented Feb 12, 2026

Implementation Complete

Summary

Refactored Linear API token handling to pass the token explicitly through the call stack instead of setting process.env.LINEAR_API_TOKEN as a side effect. All 12 functions in linear.ts now accept an optional apiToken parameter, and LinearService stores the token as an instance property.

Changes Made

  • src/utils/linear.ts: Added optional apiToken?: string parameter to 12 functions, passed to createLinearClient(apiToken)
  • src/lib/LinearService.ts: Store token as instance property, removed env var side effect, pass token to 3 call sites
  • src/lib/LinearService.test.ts: Rewrote tests to verify apiToken passing behavior instead of env var assertions

Validation Results

  • ✅ Tests: 3821 passed / 23 skipped (115 test files)
  • ✅ Typecheck: Passed
  • ✅ Lint: Passed
  • ✅ Build: Passed

Detailed Changes by File (click to expand)

src/utils/linear.ts

Changes: Added optional apiToken?: string parameter to 12 functions

  • fetchLinearIssue, createLinearIssue, createLinearChildIssue, createLinearComment
  • updateLinearIssueState, getLinearComment, updateLinearComment, fetchLinearIssueComments
  • createLinearIssueRelation, getLinearIssueDependencies, deleteLinearIssueRelation, findLinearIssueRelation
  • Each passes apiToken to createLinearClient(apiToken) which falls back to env var

src/lib/LinearService.ts

Changes: Removed env var side effect, explicit token passing

  • Added private apiToken: string | undefined instance property
  • Constructor stores this.config.apiToken instead of setting process.env.LINEAR_API_TOKEN
  • fetchIssue() passes this.apiToken to fetchLinearIssue()
  • createIssue() passes this.apiToken to createLinearIssue()
  • moveIssueToInProgress() passes this.apiToken to updateLinearIssueState()

src/lib/LinearService.test.ts

Changes: Full rewrite of test assertions

  • Removed env var save/restore logic
  • Replaced 5 env var assertion tests with 7 behavior-focused tests
  • Tests verify apiToken is passed through to linear.ts functions
  • Tests verify process.env is NOT modified as side effect

…env var

Remove process.env.LINEAR_API_TOKEN side effect from LinearService
constructor. Add optional apiToken parameter to 12 functions in
linear.ts, store token as instance property in LinearService, and
pass it explicitly to each utility call. MCP subprocess continues
using env var injection via mcp.ts.

Fixes #590
@acreeger acreeger force-pushed the refactor/issue-590__linear-api-token branch from 01f9d65 to 57054b7 Compare February 12, 2026 00:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

Refactor Linear API token: pass through stack instead of setting env var

1 participant