You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Severity: HIGH Tool: assign_milestone File: actions/setup/js/assign_milestone.cjs Analysis Date: 2026-06-10
Summary
The assign_milestone tool maintains a closure-level cache (milestoneByTitle) and an exhaustion flag (milestonesExhausted) that are keyed only by milestone title — with no repo context. In multi-repo configurations (when allowed_repos lists more than one target repository), sequential processing of messages for different repos corrupts this shared state, causing wrong milestone assignments or silent lookup failures.
// Line 72 — cache keyed by title only, no repo scopeconstmilestoneByTitle=newMap();// Line 75 — single exhaustion flag for ALL reposletmilestonesExhausted=false;asyncfunctionfindMilestoneByTitle(title,owner,repo){if(milestoneByTitle.has(title)){returnmilestoneByTitle.get(title);// ❌ Returns data from ANY previously-queried repo!}if(milestonesExhausted){returnnull;// ❌ Short-circuits even for repos never yet searched!}// ... paginate using (owner, repo) correctly ...milestoneByTitle.set(m.title,m);// ❌ Cached without repo contextif(!found){milestonesExhausted=true;// ❌ Marks ALL future repos as exhausted}}
Bug Scenarios
Scenario A — Wrong milestone number assigned (Cache Collision):
Handler processes message for org/repo1: finds milestone "v1.0" with number 5 → caches it
Handler processes message for org/repo2: looks for milestone "v1.0" (number 3 in repo2)
milestonesExhausted is already true → returns null immediately without paginating repo2
Milestone assignment fails silently even though the milestone exists in repo2
Scenario C — Misleading error messages:
allFetchedMilestones accumulates milestones from all repos. When a milestone is not found, the error message lists milestones from unrelated repos, making debugging confusing.
Detailed Analysis
Root Cause
findMilestoneByTitle receives owner and repo parameters and correctly uses them for the GitHub API pagination call. However, the closure-level cache (milestoneByTitle) and exhaustion flag (milestonesExhausted) are not scoped to a specific repo:
milestoneByTitle maps title → milestone object instead of "owner/repo::title" → milestone object
milestonesExhausted is a single boolean instead of a Set<string> of exhausted "owner/repo" identifiers
The optimization was designed for single-repo use but is used in a multi-repo handler that can process messages across different repos in one run.
When Is This Triggered?
The bug requires:
assign_milestone handler configured with multiple repos (allowed_repos with 2+ entries, or messages with different repo fields pointing to different repos)
Multiple messages processed targeting different repos
Either same milestone title in multiple repos (Scenario A) or a not-found search for one repo (Scenario B)
Single-repo configurations are not affected.
Recommended Fix
Approach: Scope all cache structures by owner/repo
Also update the auto-create cache write and the error message to use the repo-scoped structures.
Implementation Steps:
Replace let milestonesExhausted = false with const exhaustedRepos = new Set()
Replace const milestoneByTitle = new Map() key strategy: use "owner/repo::title" composite keys
Replace let allFetchedMilestones = [] with const fetchedMilestonesByRepo = new Map()
Update milestoneByTitle.set(created.data.title, created.data) (auto-create path, line 205) to use composite key
Update error message at line 209 to use fetchedMilestonesByRepo.get(repoKey) || []
Add regression tests for multi-repo milestone title collision and false-exhaustion scenarios
Alternative Solutions
Option 1: Reset cache on repo change
Track lastQueriedRepo; reset cache when repo changes between messages
Pros: Minimal code change
Cons: Loses caching benefit when messages interleave repos
Option 2: Remove the cache entirely
Always paginate fresh on each call
Pros: Eliminates all cache-related bugs
Cons: N API calls instead of 1 per distinct title (performance regression for single-repo)
Option 3: Composite key (recommended)
Correct, performant, minimal change
Testing Strategy
describe("cross-repo milestone cache isolation",()=>{it("should not return cached milestone from repo1 when querying repo2",async()=>{const{ main }=require("./assign_milestone.cjs");consthandler=awaitmain({allowed_repos: ["org/repo1","org/repo2"]});// repo1: v1.0 = #5; repo2: v1.0 = #3mockGithub.paginate.mockImplementation(async(fn,params,cb)=>{constdata=params.repo==="repo1"
? [{title: "v1.0",number: 5}]
: [{title: "v1.0",number: 3}];cb({ data },()=>{});});awaithandler({type: "assign_milestone",issue_number: 10,milestone_title: "v1.0",repo: "org/repo1"},{});awaithandler({type: "assign_milestone",issue_number: 20,milestone_title: "v1.0",repo: "org/repo2"},{});expect(mockGithub.rest.issues.update).toHaveBeenLastCalledWith(expect.objectContaining({repo: "repo2",milestone: 3})// must be 3, not 5);});it("should not skip repo2 search after repo1 exhaustion",async()=>{// repo1: no milestones; repo2: has sprint-42 = #7// After repo1 exhaustion, repo2 search must still succeed});});
Concurrency Safety Issue in
assign_milestoneSeverity: HIGH
Tool:
assign_milestoneFile:
actions/setup/js/assign_milestone.cjsAnalysis Date: 2026-06-10
Summary
The
assign_milestonetool maintains a closure-level cache (milestoneByTitle) and an exhaustion flag (milestonesExhausted) that are keyed only by milestone title — with no repo context. In multi-repo configurations (whenallowed_reposlists more than one target repository), sequential processing of messages for different repos corrupts this shared state, causing wrong milestone assignments or silent lookup failures.Issue Details
Type: Shared Mutable State — incorrect cache key scope
Location:
actions/setup/js/assign_milestone.cjslines 72–110, 205Problematic Code:
Bug Scenarios
Scenario A — Wrong milestone number assigned (Cache Collision):
org/repo1: finds milestone"v1.0"with number 5 → caches itorg/repo2: looks for milestone"v1.0"(number 3 in repo2)issues.updatecall assigns milestone Add workflow: githubnext/agentics/weekly-research #5 to a repo2 issue — wrong milestone, silent failureScenario B — Milestone not found when it exists (False Exhaustion):
org/repo1: milestone"sprint-42"not found →milestonesExhausted = trueorg/repo2: looks for milestone"sprint-42"(exists in repo2 as Weekly Research Report: AI Workflow Automation Landscape and Market Opportunities - August 2025 #7)milestonesExhaustedis alreadytrue→ returnsnullimmediately without paginating repo2Scenario C — Misleading error messages:
allFetchedMilestonesaccumulates milestones from all repos. When a milestone is not found, the error message lists milestones from unrelated repos, making debugging confusing.Detailed Analysis
Root Cause
findMilestoneByTitlereceivesownerandrepoparameters and correctly uses them for the GitHub API pagination call. However, the closure-level cache (milestoneByTitle) and exhaustion flag (milestonesExhausted) are not scoped to a specific repo:milestoneByTitlemapstitle → milestone objectinstead of"owner/repo::title" → milestone objectmilestonesExhaustedis a singlebooleaninstead of aSet<string>of exhausted"owner/repo"identifiersThe optimization was designed for single-repo use but is used in a multi-repo handler that can process messages across different repos in one run.
When Is This Triggered?
The bug requires:
assign_milestonehandler configured with multiple repos (allowed_reposwith 2+ entries, or messages with differentrepofields pointing to different repos)Single-repo configurations are not affected.
Recommended Fix
Approach: Scope all cache structures by
owner/repoAlso update the auto-create cache write and the error message to use the repo-scoped structures.
Implementation Steps:
let milestonesExhausted = falsewithconst exhaustedRepos = new Set()const milestoneByTitle = new Map()key strategy: use"owner/repo::title"composite keyslet allFetchedMilestones = []withconst fetchedMilestonesByRepo = new Map()milestoneByTitle.set(created.data.title, created.data)(auto-create path, line 205) to use composite keyfetchedMilestonesByRepo.get(repoKey) || []Alternative Solutions
Option 1: Reset cache on repo change
lastQueriedRepo; reset cache when repo changes between messagesOption 2: Remove the cache entirely
Option 3: Composite key (recommended)
Testing Strategy
References:
create_issue(createdTitlesByRepo,parentIssueCache)actions/setup/js/assign_milestone.cjsPriority: P1-High
Effort: Small (< 1 day)
Expected Impact: Prevents silent wrong-milestone assignments and missed lookups in multi-repo
assign_milestoneconfigurations