Skip to content

[concurrency] 🚨 Concurrency Safety: Shared Mutable State in create_issue Tool #33514

@github-actions

Description

@github-actions

Concurrency Safety Issue in create_issue

Severity: CRITICAL
Tool: create_issue
File: actions/setup/js/create_issue.cjs
Analysis Date: 2026-05-20

Summary

The create_issue tool contains three shared mutable Map objects that are accessed and modified across multiple concurrent tool invocations without any synchronization. This creates critical race conditions that can lead to:

  • Lost parent issue assignments (multiple parent issues created for same group)
  • Lost temporary ID mappings (broken cross-references between issues)
  • Incorrect deduplication state (duplicate issues created despite dedup being enabled)

Issue Details

Type: Shared Mutable Data Structures

Locations:

  1. create_issue.cjs:635parentIssueCache = new Map()
  2. create_issue.cjs:632temporaryIdMap = new Map()
  3. create_issue.cjs:614createdTitlesByRepo = new Map()
  4. create_issue.cjs:616repoTitleDedupCandidatesCache = new Map()

Code Pattern:

// ❌ UNSAFE: Shared mutable Maps accessed by all concurrent invocations
function main(config, context, github, core) {
  // ...
  const temporaryIdMap = new Map();          // Line 632
  const createdTitlesByRepo = new Map();     // Line 614
  const repoTitleDedupCandidatesCache = new Map();  // Line 616
  const parentIssueCache = new Map();        // Line 635

  return async function handleCreateIssue(message, resolvedTemporaryIds) {
    // Multiple concurrent calls share these Maps!
    
    // Race 1: Parent issue cache
    let groupParentNumber = parentIssueCache.get(groupId);  // Line 1123
    if (!groupParentNumber) {
      groupParentNumber = await findOrCreateParentIssue({...});  // Line 1129
      parentIssueCache.set(groupId, groupParentNumber);  // Line 1143 - RACE!
    }
    
    // Race 2: Temporary ID map
    temporaryIdMap.set(normalizedTempId, { repo, number });  // Line 1065 - RACE!
    
    // Race 3: Title dedup tracking
    const titles = createdTitlesByRepo.get(repo) || [];  // Line 626 - RACE!
    titles.push({ title, normalizedTitle });  // Line 627 - RACE!
    createdTitlesByRepo.set(repo, titles);  // Line 628 - RACE!
  };
}

Race Condition Scenarios: See detailed timeline analysis in full issue body.

Root Cause

JavaScript Concurrency Model: Node.js single-threaded event loop creates interleaving through async/await. When a function awaits, control returns to event loop, allowing other async functions to execute and access shared mutable state.

Recommended Fix

Approach: State Isolation — Move Maps into batch-scoped context

// ✅ SAFE: Properly isolated state
function main(config, context, github, core) {
  return function createBatchHandler() {
    // Fresh state for each batch
    const temporaryIdMap = new Map();
    const parentIssueCache = new Map();
    // ...
    
    return async function handleCreateIssue(message) {
      // Isolated state per batch!
    };
  };
}

Testing Strategy

Verify fix with concurrent execution tests:

test('concurrent calls without race conditions', async () => {
  const batchHandler = handler();
  const promises = Array(10).fill(0).map(() => batchHandler(msg));
  const results = await Promise.all(promises);
  
  // Verify exactly one parent per group
  expect(uniqueParents.size).toBe(1);
});

Priority: P0-Critical
Effort: Medium (2-3 hours)
Expected Impact: Prevents data races causing lost updates and broken cross-references

Generated by 📊 Daily MCP Tool Concurrency Analysis · ● 7.3M ·

  • expires on May 27, 2026, 10:22 AM UTC

Metadata

Metadata

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions