From e4434f1b7db244d302dc1bcff9d32cc447693b2b Mon Sep 17 00:00:00 2001 From: Leonid Bugaev Date: Fri, 1 May 2026 15:02:30 +0000 Subject: [PATCH 01/18] fix(branch-suggestion): output critical JSON issue on JIRA error and add missing tests --- branch-suggestion/branch_suggestion.yml | 5 +- .../jira/__tests__/get-fixedversion.test.js | 46 ++++++++++++++++++- 2 files changed, 46 insertions(+), 5 deletions(-) diff --git a/branch-suggestion/branch_suggestion.yml b/branch-suggestion/branch_suggestion.yml index 5044bef..b5993b8 100644 --- a/branch-suggestion/branch_suggestion.yml +++ b/branch-suggestion/branch_suggestion.yml @@ -35,10 +35,9 @@ checks: echo "ℹ️ No JIRA ticket found. Skipping branch suggestions." >&2 exit 0 fi - if [ $JIRA_EXIT_CODE -eq 1 ]; then - echo "❌ JIRA ticket found but no fix versions set." >&2 - exit 1 + echo '[{"file": "branch-suggestion", "line": 0, "message": "JIRA ticket found but no fix versions set, or JIRA API error (e.g. invalid token).", "severity": "critical", "ruleId": "command/execution_error"}]' + exit 0 fi # Fetch repository branches diff --git a/branch-suggestion/scripts/jira/__tests__/get-fixedversion.test.js b/branch-suggestion/scripts/jira/__tests__/get-fixedversion.test.js index 71b1564..21a7abe 100644 --- a/branch-suggestion/scripts/jira/__tests__/get-fixedversion.test.js +++ b/branch-suggestion/scripts/jira/__tests__/get-fixedversion.test.js @@ -1,5 +1,6 @@ -import { describe, it, expect } from 'vitest'; -import { extractJiraTicket, parseVersion, detectComponent } from '../get-fixedversion.js'; +import { describe, it, expect, vi } from 'vitest'; +import { extractJiraTicket, parseVersion, detectComponent, getFixVersions } from '../get-fixedversion.js'; +import * as jiraApi from '../jira-api.js'; describe('extractJiraTicket', () => { it('should extract ticket from PR title', () => { @@ -145,3 +146,44 @@ describe('parseVersion', () => { expect(result.original).toBe('TIB 1.7.0'); }); }); + +// Mock the JIRA API module +vi.mock('../jira-api.js', () => ({ + getIssue: vi.fn() +})); + +describe('getFixVersions', () => { + it('should return fix versions when present', async () => { + vi.mocked(jiraApi.getIssue).mockResolvedValue({ + fields: { + summary: 'Test issue', + priority: { name: 'High' }, + issuetype: { name: 'Bug' }, + fixVersions: [{ name: '1.0.0', id: '10000', released: false }] + } + }); + + const result = await getFixVersions('TT-123'); + expect(result.ticket).toBe('TT-123'); + expect(result.fixVersions).toHaveLength(1); + expect(result.fixVersions[0].name).toBe('1.0.0'); + }); + + it('should handle missing fix versions', async () => { + vi.mocked(jiraApi.getIssue).mockResolvedValue({ + fields: { + summary: 'Test issue', + fixVersions: [] + } + }); + + const result = await getFixVersions('TT-123'); + expect(result.fixVersions).toHaveLength(0); + }); + + it('should throw error when JIRA API fails (e.g. invalid token)', async () => { + vi.mocked(jiraApi.getIssue).mockRejectedValue(new Error('JIRA_TOKEN must be set')); + + await expect(getFixVersions('TT-123')).rejects.toThrow('Failed to fetch JIRA ticket TT-123: JIRA_TOKEN must be set'); + }); +}); From 7bd153a06359989f653c88db3c0ad3f935942a67 Mon Sep 17 00:00:00 2001 From: Leonid Bugaev Date: Fri, 1 May 2026 15:30:37 +0000 Subject: [PATCH 02/18] fix: conditionally checkout PR branch for branch-suggestion workflow --- .github/workflows/branch-suggestion.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/branch-suggestion.yml b/.github/workflows/branch-suggestion.yml index 1abe937..5328dea 100644 --- a/.github/workflows/branch-suggestion.yml +++ b/.github/workflows/branch-suggestion.yml @@ -16,13 +16,12 @@ permissions: jobs: suggest-branches: runs-on: ubuntu-latest - steps: - name: Checkout github-actions repository uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: - repository: TykTechnologies/github-actions - ref: main + repository: ${{ github.repository == 'TykTechnologies/github-actions' && github.event.pull_request.head.repo.full_name || 'TykTechnologies/github-actions' }} + ref: ${{ github.repository == 'TykTechnologies/github-actions' && github.event.pull_request.head.sha || 'main' }} - name: Setup Node.js uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 From cd04172f8d181bbfd12a238fca5e8342aa7c72dc Mon Sep 17 00:00:00 2001 From: Leonid Bugaev Date: Fri, 1 May 2026 15:42:53 +0000 Subject: [PATCH 03/18] fix: add tags: ["remote"] to analyze-and-suggest and revert test file changes --- branch-suggestion/branch_suggestion.yml | 2 +- .../jira/__tests__/get-fixedversion.test.js | 46 +------------------ 2 files changed, 3 insertions(+), 45 deletions(-) diff --git a/branch-suggestion/branch_suggestion.yml b/branch-suggestion/branch_suggestion.yml index b5993b8..0b0e87e 100644 --- a/branch-suggestion/branch_suggestion.yml +++ b/branch-suggestion/branch_suggestion.yml @@ -13,9 +13,9 @@ checks: # 2. Fetch fix versions from JIRA # 3. Fetch repository branches # 4. Match and suggest branches - # ============================================================================ analyze-and-suggest: type: command + tags: ["remote"] timeout: 90 exec: | set -e diff --git a/branch-suggestion/scripts/jira/__tests__/get-fixedversion.test.js b/branch-suggestion/scripts/jira/__tests__/get-fixedversion.test.js index 21a7abe..71b1564 100644 --- a/branch-suggestion/scripts/jira/__tests__/get-fixedversion.test.js +++ b/branch-suggestion/scripts/jira/__tests__/get-fixedversion.test.js @@ -1,6 +1,5 @@ -import { describe, it, expect, vi } from 'vitest'; -import { extractJiraTicket, parseVersion, detectComponent, getFixVersions } from '../get-fixedversion.js'; -import * as jiraApi from '../jira-api.js'; +import { describe, it, expect } from 'vitest'; +import { extractJiraTicket, parseVersion, detectComponent } from '../get-fixedversion.js'; describe('extractJiraTicket', () => { it('should extract ticket from PR title', () => { @@ -146,44 +145,3 @@ describe('parseVersion', () => { expect(result.original).toBe('TIB 1.7.0'); }); }); - -// Mock the JIRA API module -vi.mock('../jira-api.js', () => ({ - getIssue: vi.fn() -})); - -describe('getFixVersions', () => { - it('should return fix versions when present', async () => { - vi.mocked(jiraApi.getIssue).mockResolvedValue({ - fields: { - summary: 'Test issue', - priority: { name: 'High' }, - issuetype: { name: 'Bug' }, - fixVersions: [{ name: '1.0.0', id: '10000', released: false }] - } - }); - - const result = await getFixVersions('TT-123'); - expect(result.ticket).toBe('TT-123'); - expect(result.fixVersions).toHaveLength(1); - expect(result.fixVersions[0].name).toBe('1.0.0'); - }); - - it('should handle missing fix versions', async () => { - vi.mocked(jiraApi.getIssue).mockResolvedValue({ - fields: { - summary: 'Test issue', - fixVersions: [] - } - }); - - const result = await getFixVersions('TT-123'); - expect(result.fixVersions).toHaveLength(0); - }); - - it('should throw error when JIRA API fails (e.g. invalid token)', async () => { - vi.mocked(jiraApi.getIssue).mockRejectedValue(new Error('JIRA_TOKEN must be set')); - - await expect(getFixVersions('TT-123')).rejects.toThrow('Failed to fetch JIRA ticket TT-123: JIRA_TOKEN must be set'); - }); -}); From e8a756b2f0817a141b039b96a8b736560dd51a09 Mon Sep 17 00:00:00 2001 From: Leonid Bugaev Date: Fri, 1 May 2026 16:08:23 +0000 Subject: [PATCH 04/18] fix(branch-suggestion): separate JIRA API errors from missing fix versions --- branch-suggestion/branch_suggestion.yml | 10 +++++++--- branch-suggestion/scripts/jira/get-fixedversion.js | 5 +++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/branch-suggestion/branch_suggestion.yml b/branch-suggestion/branch_suggestion.yml index 0b0e87e..5689df7 100644 --- a/branch-suggestion/branch_suggestion.yml +++ b/branch-suggestion/branch_suggestion.yml @@ -36,7 +36,11 @@ checks: exit 0 fi if [ $JIRA_EXIT_CODE -eq 1 ]; then - echo '[{"file": "branch-suggestion", "line": 0, "message": "JIRA ticket found but no fix versions set, or JIRA API error (e.g. invalid token).", "severity": "critical", "ruleId": "command/execution_error"}]' + echo '[{"file": "branch-suggestion", "line": 0, "message": "JIRA API or configuration error (e.g. invalid token).", "severity": "critical", "ruleId": "command/execution_error"}]' + exit 0 + fi + if [ $JIRA_EXIT_CODE -eq 3 ]; then + echo '[{"file": "branch-suggestion", "line": 0, "message": "JIRA ticket found but no fix versions set.", "severity": "info", "ruleId": "command/execution_info"}]' exit 0 fi @@ -86,8 +90,8 @@ checks: # Check if markdown file exists from previous step if [ ! -f /tmp/branch_suggestion_markdown.txt ]; then - echo "❌ Error: Markdown file not found. analyze-and-suggest step may have failed." >&2 - exit 1 + echo "ℹ️ Markdown file not found. analyze-and-suggest step may have exited early with info severity." >&2 + exit 0 fi # Post or update the comment diff --git a/branch-suggestion/scripts/jira/get-fixedversion.js b/branch-suggestion/scripts/jira/get-fixedversion.js index 57dfba7..07de8ff 100644 --- a/branch-suggestion/scripts/jira/get-fixedversion.js +++ b/branch-suggestion/scripts/jira/get-fixedversion.js @@ -140,8 +140,9 @@ async function main() { console.log('\nOutput: JSON object with ticket info and fix versions'); console.log('\nExit codes:'); console.log(' 0 - Success (fix versions found)'); - console.log(' 1 - Error (ticket found but no fix versions set)'); + console.log(' 1 - Error (JIRA API or configuration error)'); console.log(' 2 - No JIRA ticket found'); + console.log(' 3 - Info (ticket found but no fix versions set)'); process.exit(1); } @@ -186,7 +187,7 @@ async function main() { priority: result.priority, issueType: result.issueType })); - process.exit(1); + process.exit(3); } console.log(JSON.stringify(result, null, 2)); From 19b1c71839ec89fc43beafaa9d796df62f572598 Mon Sep 17 00:00:00 2001 From: Leonid Bugaev Date: Fri, 1 May 2026 16:19:45 +0000 Subject: [PATCH 05/18] test: add unit tests for getFixVersions and exit code logic --- .../jira/__tests__/get-fixedversion.test.js | 146 +++++++++++++++++- .../scripts/jira/get-fixedversion.js | 4 +- 2 files changed, 146 insertions(+), 4 deletions(-) diff --git a/branch-suggestion/scripts/jira/__tests__/get-fixedversion.test.js b/branch-suggestion/scripts/jira/__tests__/get-fixedversion.test.js index 71b1564..d337c4c 100644 --- a/branch-suggestion/scripts/jira/__tests__/get-fixedversion.test.js +++ b/branch-suggestion/scripts/jira/__tests__/get-fixedversion.test.js @@ -1,5 +1,10 @@ -import { describe, it, expect } from 'vitest'; -import { extractJiraTicket, parseVersion, detectComponent } from '../get-fixedversion.js'; +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { extractJiraTicket, parseVersion, detectComponent, getFixVersions, main } from '../get-fixedversion.js'; +import { getIssue } from '../jira-api.js'; + +vi.mock('../jira-api.js', () => ({ + getIssue: vi.fn() +})); describe('extractJiraTicket', () => { it('should extract ticket from PR title', () => { @@ -145,3 +150,140 @@ describe('parseVersion', () => { expect(result.original).toBe('TIB 1.7.0'); }); }); + +describe('getFixVersions', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should return parsed fix versions on success', async () => { + getIssue.mockResolvedValue({ + fields: { + summary: 'Test issue', + priority: { name: 'High' }, + issuetype: { name: 'Bug' }, + fixVersions: [ + { name: 'Tyk 5.0.0', id: '10000', released: true } + ] + } + }); + + const result = await getFixVersions('TT-123'); + expect(result).toEqual({ + ticket: 'TT-123', + summary: 'Test issue', + priority: 'High', + issueType: 'Bug', + fixVersions: [ + { + name: 'Tyk 5.0.0', + id: '10000', + released: true, + parsed: { + major: 5, + minor: 0, + patch: 0, + original: 'Tyk 5.0.0', + component: ['tyk', 'tyk-analytics', 'tyk-analytics-ui'] + } + } + ] + }); + expect(getIssue).toHaveBeenCalledWith('TT-123'); + }); + + it('should handle missing fix versions', async () => { + getIssue.mockResolvedValue({ + fields: { + summary: 'Test issue', + priority: { name: 'High' }, + issuetype: { name: 'Bug' }, + // no fixVersions + } + }); + + const result = await getFixVersions('TT-123'); + expect(result.fixVersions).toEqual([]); + }); + + it('should throw error when getIssue fails', async () => { + getIssue.mockRejectedValue(new Error('API Error')); + + await expect(getFixVersions('TT-123')).rejects.toThrow('Failed to fetch JIRA ticket TT-123: API Error'); + }); +}); + +describe('main execution', () => { + let originalArgv; + let exitMock; + let consoleLogMock; + let consoleErrorMock; + + beforeEach(() => { + originalArgv = process.argv; + exitMock = vi.spyOn(process, 'exit').mockImplementation(() => {}); + consoleLogMock = vi.spyOn(console, 'log').mockImplementation(() => {}); + consoleErrorMock = vi.spyOn(console, 'error').mockImplementation(() => {}); + vi.clearAllMocks(); + }); + + afterEach(() => { + process.argv = originalArgv; + vi.restoreAllMocks(); + }); + + it('should exit with code 3 if no fix versions found', async () => { + process.argv = ['node', 'script.js', 'TT-123']; + getIssue.mockResolvedValue({ + fields: { + summary: 'Test', + fixVersions: [] + } + }); + + await main(); + + expect(consoleErrorMock).toHaveBeenCalled(); + const errorCall = consoleErrorMock.mock.calls[0][0]; + expect(errorCall).toContain('No fix versions found in JIRA ticket'); + expect(exitMock).toHaveBeenCalledWith(3); + }); + + it('should exit with code 1 on API error', async () => { + process.argv = ['node', 'script.js', 'TT-123']; + getIssue.mockRejectedValue(new Error('API Error')); + + await main(); + + expect(consoleErrorMock).toHaveBeenCalled(); + const errorCall = consoleErrorMock.mock.calls[0][0]; + expect(errorCall).toContain('API Error'); + expect(exitMock).toHaveBeenCalledWith(1); + }); + + it('should exit with code 2 if no ticket found', async () => { + process.argv = ['node', 'script.js', 'No ticket here']; + + await main(); + + expect(consoleErrorMock).toHaveBeenCalled(); + const errorCall = consoleErrorMock.mock.calls[0][0]; + expect(errorCall).toContain('No JIRA ticket found'); + expect(exitMock).toHaveBeenCalledWith(2); + }); + + it('should log result and not exit on success', async () => { + process.argv = ['node', 'script.js', 'TT-123']; + getIssue.mockResolvedValue({ + fields: { + summary: 'Test', + fixVersions: [{ name: '1.0.0' }] + } + }); + + await main(); + + expect(consoleLogMock).toHaveBeenCalled(); + expect(exitMock).not.toHaveBeenCalled(); + }); +}); diff --git a/branch-suggestion/scripts/jira/get-fixedversion.js b/branch-suggestion/scripts/jira/get-fixedversion.js index 07de8ff..99413ac 100644 --- a/branch-suggestion/scripts/jira/get-fixedversion.js +++ b/branch-suggestion/scripts/jira/get-fixedversion.js @@ -201,13 +201,13 @@ async function main() { } } - // Export functions for use in other scripts export { extractJiraTicket, getFixVersions, parseVersion, - detectComponent + detectComponent, + main }; From 6b8005addd26afbb852e126222349547b3c53ce2 Mon Sep 17 00:00:00 2001 From: Leonid Bugaev Date: Fri, 1 May 2026 16:37:19 +0000 Subject: [PATCH 06/18] test: add missing tests for jira-api.js and get-fixedversion.js --- .../jira/__tests__/get-fixedversion.test.js | 57 ++++ .../scripts/jira/__tests__/jira-api.test.js | 246 ++++++++++++++++++ branch-suggestion/scripts/jira/jira-api.js | 12 +- 3 files changed, 310 insertions(+), 5 deletions(-) create mode 100644 branch-suggestion/scripts/jira/__tests__/jira-api.test.js diff --git a/branch-suggestion/scripts/jira/__tests__/get-fixedversion.test.js b/branch-suggestion/scripts/jira/__tests__/get-fixedversion.test.js index d337c4c..1ab380a 100644 --- a/branch-suggestion/scripts/jira/__tests__/get-fixedversion.test.js +++ b/branch-suggestion/scripts/jira/__tests__/get-fixedversion.test.js @@ -286,4 +286,61 @@ describe('main execution', () => { expect(consoleLogMock).toHaveBeenCalled(); expect(exitMock).not.toHaveBeenCalled(); }); + + it('should exit with code 1 if no arguments provided', async () => { + process.argv = ['node', 'script.js']; + + await main(); + + expect(consoleLogMock).toHaveBeenCalledWith(expect.stringContaining('Usage:')); + expect(exitMock).toHaveBeenCalledWith(1); + }); + + it('should extract ticket from branchName (Priority 1)', async () => { + process.argv = ['node', 'script.js', 'No ticket here', 'feature/TT-999-fix']; + getIssue.mockResolvedValue({ + fields: { + summary: 'Test', + fixVersions: [{ name: '1.0.0' }] + } + }); + + await main(); + + expect(getIssue).toHaveBeenCalledWith('TT-999'); + expect(consoleLogMock).toHaveBeenCalled(); + expect(exitMock).not.toHaveBeenCalled(); + }); + + it('should extract ticket from prTitle (Priority 2)', async () => { + process.argv = ['node', 'script.js', 'TT-888: Fix bug', 'no-ticket-branch']; + getIssue.mockResolvedValue({ + fields: { + summary: 'Test', + fixVersions: [{ name: '1.0.0' }] + } + }); + + await main(); + + expect(getIssue).toHaveBeenCalledWith('TT-888'); + expect(consoleLogMock).toHaveBeenCalled(); + expect(exitMock).not.toHaveBeenCalled(); + }); + + it('should handle prTitle as direct ticket key', async () => { + process.argv = ['node', 'script.js', 'TT-777']; + getIssue.mockResolvedValue({ + fields: { + summary: 'Test', + fixVersions: [{ name: '1.0.0' }] + } + }); + + await main(); + + expect(getIssue).toHaveBeenCalledWith('TT-777'); + expect(consoleLogMock).toHaveBeenCalled(); + expect(exitMock).not.toHaveBeenCalled(); + }); }); diff --git a/branch-suggestion/scripts/jira/__tests__/jira-api.test.js b/branch-suggestion/scripts/jira/__tests__/jira-api.test.js new file mode 100644 index 0000000..d695ba2 --- /dev/null +++ b/branch-suggestion/scripts/jira/__tests__/jira-api.test.js @@ -0,0 +1,246 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; + +// Set env var before importing +process.env.JIRA_TOKEN = 'test-token'; + +import { extractJQL, jiraAPI, searchIssues, getIssue, formatIssue, main } from '../jira-api.js'; +import readline from 'readline'; + +// Mock fetch +global.fetch = vi.fn(); + +vi.mock('readline', () => ({ + default: { + createInterface: vi.fn() + } +})); + +describe('extractJQL', () => { + it('should extract JQL from a valid JIRA URL', () => { + const url = 'https://tyktech.atlassian.net/jira/software/c/projects/TT/issues/?jql=project%20%3D%20TT%20AND%20status%20!%3D%20closed'; + expect(extractJQL(url)).toBe('project = TT AND status != closed'); + }); + + it('should return input if it is not a URL but contains jql=', () => { + const input = 'jql=project = TT'; + // URL parsing will fail, so it returns input + expect(extractJQL(input)).toBe(input); + }); + + it('should return input if it is a direct JQL query', () => { + const input = 'project = TT AND status != closed'; + expect(extractJQL(input)).toBe(input); + }); + + it('should return input if URL does not contain jql parameter', () => { + const url = 'https://tyktech.atlassian.net/browse/TT-123'; + expect(extractJQL(url)).toBe(url); + }); +}); + +describe('jiraAPI', () => { + const originalEnv = process.env; + + beforeEach(() => { + vi.resetModules(); + process.env = { ...originalEnv }; + global.fetch.mockClear(); + }); + + afterEach(() => { + process.env = originalEnv; + }); + + it('should throw error if JIRA_TOKEN is not set', async () => { + delete process.env.JIRA_TOKEN; + await expect(jiraAPI('/test')).rejects.toThrow('JIRA_TOKEN must be set'); + }); + + it('should handle 401 Unauthorized error', async () => { + global.fetch.mockResolvedValue({ + ok: false, + status: 401, + text: vi.fn().mockResolvedValue('Unauthorized') + }); + + await expect(jiraAPI('/test')).rejects.toThrow('JIRA API Error (401): Unauthorized'); + }); + + it('should handle 404 Not Found error', async () => { + global.fetch.mockResolvedValue({ + ok: false, + status: 404, + text: vi.fn().mockResolvedValue('Issue does not exist') + }); + + await expect(jiraAPI('/test')).rejects.toThrow('JIRA API Error (404): Issue does not exist'); + }); + + it('should return JSON on success', async () => { + const mockData = { key: 'TT-123' }; + global.fetch.mockResolvedValue({ + ok: true, + json: vi.fn().mockResolvedValue(mockData) + }); + + const result = await jiraAPI('/test'); + expect(result).toEqual(mockData); + }); +}); + +describe('searchIssues', () => { + beforeEach(() => { + global.fetch.mockClear(); + }); + + it('should call jiraAPI with correct parameters', async () => { + global.fetch.mockResolvedValue({ + ok: true, + json: vi.fn().mockResolvedValue({ issues: [] }) + }); + + await searchIssues('project = TT', 10, 20); + + expect(global.fetch).toHaveBeenCalledTimes(1); + const url = global.fetch.mock.calls[0][0]; + expect(url).toContain('/search/jql?'); + expect(url).toContain('jql=project+%3D+TT'); + expect(url).toContain('startAt=10'); + expect(url).toContain('maxResults=20'); + }); +}); + +describe('formatIssue', () => { + it('should format issue correctly', () => { + const issue = { + key: 'TT-123', + fields: { + summary: 'Test issue', + status: { name: 'In Progress' }, + issuetype: { name: 'Bug' }, + priority: { name: 'High' }, + created: '2023-01-01T00:00:00.000Z', + assignee: { displayName: 'John Doe' }, + reporter: { displayName: 'Jane Doe' }, + labels: ['test', 'bug'], + components: [{ name: 'API' }] + } + }; + + const formatted = formatIssue(issue, 0); + expect(formatted).toContain('1. [TT-123] Test issue'); + expect(formatted).toContain('Status: In Progress'); + expect(formatted).toContain('Type: Bug'); + expect(formatted).toContain('Priority: High'); + expect(formatted).toContain('Assignee: John Doe'); + expect(formatted).toContain('Reporter: Jane Doe'); + expect(formatted).toContain('Labels: test, bug'); + expect(formatted).toContain('Components: API'); + expect(formatted).toContain('Link: https://tyktech.atlassian.net/browse/TT-123'); + }); + + it('should handle missing fields', () => { + const issue = { + key: 'TT-123', + fields: {} + }; + + const formatted = formatIssue(issue, 0); + expect(formatted).toContain('1. [TT-123] No summary'); + expect(formatted).toContain('Status: Unknown'); + expect(formatted).toContain('Type: Unknown'); + expect(formatted).toContain('Priority: None'); + expect(formatted).toContain('Created: Unknown'); + }); +}); + +describe('main execution', () => { + let originalArgv; + let exitMock; + let consoleLogMock; + let consoleErrorMock; + let stdoutWriteMock; + + beforeEach(() => { + originalArgv = process.argv; + exitMock = vi.spyOn(process, 'exit').mockImplementation(() => {}); + consoleLogMock = vi.spyOn(console, 'log').mockImplementation(() => {}); + consoleErrorMock = vi.spyOn(console, 'error').mockImplementation(() => {}); + stdoutWriteMock = vi.spyOn(process.stdout, 'write').mockImplementation(() => {}); + vi.clearAllMocks(); + global.fetch.mockClear(); + }); + + afterEach(() => { + process.argv = originalArgv; + vi.restoreAllMocks(); + }); + + it('should exit with code 1 if no arguments provided', async () => { + process.argv = ['node', 'script.js']; + + await main(); + + expect(consoleLogMock).toHaveBeenCalledWith(expect.stringContaining('Usage:')); + expect(exitMock).toHaveBeenCalledWith(1); + }); + + it('should handle pagination when total > pageSize and user answers yes', async () => { + process.argv = ['node', 'script.js', 'project = TT']; + + // Mock first page + global.fetch.mockResolvedValueOnce({ + ok: true, + json: vi.fn().mockResolvedValue({ + total: 100, + issues: Array(50).fill({ key: 'TT-1', fields: { summary: 'Test' } }) + }) + }); + + // Mock second page + global.fetch.mockResolvedValueOnce({ + ok: true, + json: vi.fn().mockResolvedValue({ + total: 100, + issues: Array(50).fill({ key: 'TT-2', fields: { summary: 'Test 2' } }) + }) + }); + + // Mock readline to answer 'yes' + const questionMock = vi.fn((query, cb) => cb('yes')); + readline.createInterface.mockReturnValue({ + question: questionMock, + close: vi.fn() + }); + + await main(); + + expect(global.fetch).toHaveBeenCalledTimes(2); + expect(consoleLogMock).toHaveBeenCalledWith(expect.stringContaining('Displayed 100 of 100 total issues')); + }); + + it('should not fetch remaining pages if user answers no', async () => { + process.argv = ['node', 'script.js', 'project = TT']; + + // Mock first page + global.fetch.mockResolvedValueOnce({ + ok: true, + json: vi.fn().mockResolvedValue({ + total: 100, + issues: Array(50).fill({ key: 'TT-1', fields: { summary: 'Test' } }) + }) + }); + + // Mock readline to answer 'no' + const questionMock = vi.fn((query, cb) => cb('no')); + readline.createInterface.mockReturnValue({ + question: questionMock, + close: vi.fn() + }); + + await main(); + + expect(global.fetch).toHaveBeenCalledTimes(1); + expect(consoleLogMock).toHaveBeenCalledWith(expect.stringContaining('Displayed 50 of 100 total issues')); + }); +}); \ No newline at end of file diff --git a/branch-suggestion/scripts/jira/jira-api.js b/branch-suggestion/scripts/jira/jira-api.js index 364f540..3a17b9d 100755 --- a/branch-suggestion/scripts/jira/jira-api.js +++ b/branch-suggestion/scripts/jira/jira-api.js @@ -10,11 +10,11 @@ if (!process.env.JIRA_TOKEN) { // JIRA configuration const JIRA_BASE_URL = 'https://tyktech.atlassian.net'; -const JIRA_TOKEN = process.env.JIRA_TOKEN; // Pre-encoded base64(email:api_token) +// We read JIRA_TOKEN dynamically inside jiraAPI to allow testing // Debug logging (without exposing sensitive data) console.error('DEBUG: Environment check:'); -console.error(` JIRA_TOKEN: ${JIRA_TOKEN ? 'SET' : 'EMPTY'}`); +console.error(` JIRA_TOKEN: ${process.env.JIRA_TOKEN ? 'SET' : 'EMPTY'}`); console.error(` All JIRA env vars: ${Object.keys(process.env).filter(k => k.includes('JIRA')).join(', ')}`); // Extract JQL from URL or use directly @@ -37,12 +37,13 @@ function extractJQL(input) { // Make JIRA API request async function jiraAPI(endpoint, options = {}) { - if (!JIRA_TOKEN) { + const token = process.env.JIRA_TOKEN; + if (!token) { throw new Error('JIRA_TOKEN must be set in .env file (pre-encoded base64(email:api_token))'); } // Token is already base64 encoded, use directly - const auth = JIRA_TOKEN; + const auth = token; const response = await fetch(`${JIRA_BASE_URL}/rest/api/3${endpoint}`, { ...options, @@ -260,7 +261,8 @@ export { searchIssues, getIssue, formatIssue, - exportToCSV + exportToCSV, + main }; // Run main if executed directly From 709d50543a62c27863c94a0fe1fb3f958d810637 Mon Sep 17 00:00:00 2001 From: Leonid Bugaev Date: Fri, 1 May 2026 16:56:53 +0000 Subject: [PATCH 07/18] chore: remove dotenv dependency from branch-suggestion --- branch-suggestion/package-lock.json | 13 ------------- branch-suggestion/package.json | 1 - branch-suggestion/scripts/common/match-branches.js | 8 -------- branch-suggestion/scripts/github/add-pr-comment.js | 5 ----- branch-suggestion/scripts/github/github-api.js | 3 --- branch-suggestion/scripts/jira/get-fixedversion.js | 7 ------- branch-suggestion/scripts/jira/jira-api.js | 5 ----- 7 files changed, 42 deletions(-) diff --git a/branch-suggestion/package-lock.json b/branch-suggestion/package-lock.json index b73ba68..fd67bc4 100644 --- a/branch-suggestion/package-lock.json +++ b/branch-suggestion/package-lock.json @@ -13,7 +13,6 @@ "@anthropic-ai/sdk": "^0.60.0", "@modelcontextprotocol/sdk": "^1.17.4", "commander": "^14.0.0", - "dotenv": "^17.2.1", "mcp-remote": "^0.1.18", "turndown": "^7.2.1" }, @@ -1797,18 +1796,6 @@ "npm": "1.2.8000 || >= 1.4.16" } }, - "node_modules/dotenv": { - "version": "17.2.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.1.tgz", - "integrity": "sha512-kQhDYKZecqnM0fCnzI5eIv5L4cAe/iRI+HqMbO/hbRdTAeXDG+M9FjipUxNfbARuEg4iHIbhnhs78BCHNbSxEQ==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", diff --git a/branch-suggestion/package.json b/branch-suggestion/package.json index 19cb2e6..26c4326 100644 --- a/branch-suggestion/package.json +++ b/branch-suggestion/package.json @@ -22,7 +22,6 @@ "@anthropic-ai/sdk": "^0.60.0", "@modelcontextprotocol/sdk": "^1.17.4", "commander": "^14.0.0", - "dotenv": "^17.2.1", "mcp-remote": "^0.1.18", "turndown": "^7.2.1" }, diff --git a/branch-suggestion/scripts/common/match-branches.js b/branch-suggestion/scripts/common/match-branches.js index 08d14db..c669c65 100644 --- a/branch-suggestion/scripts/common/match-branches.js +++ b/branch-suggestion/scripts/common/match-branches.js @@ -1,12 +1,4 @@ #!/usr/bin/env node -import dotenv from 'dotenv'; - -// Only load .env if JIRA_TOKEN is not already set (to avoid log output in CI) -// Silence dotenv v17+ logging -if (!process.env.JIRA_TOKEN) { - process.env.DOTENV_LOG_LEVEL = 'error'; - dotenv.config(); -} /** * Generate candidate branch names based on a parsed version diff --git a/branch-suggestion/scripts/github/add-pr-comment.js b/branch-suggestion/scripts/github/add-pr-comment.js index 94af2bf..e344950 100644 --- a/branch-suggestion/scripts/github/add-pr-comment.js +++ b/branch-suggestion/scripts/github/add-pr-comment.js @@ -1,5 +1,4 @@ #!/usr/bin/env node -import dotenv from 'dotenv'; import fs from 'fs'; import { getPullRequest, @@ -8,10 +7,6 @@ import { findCommentByMarker } from './github-api.js'; -// Silence dotenv v17+ logging -process.env.DOTENV_LOG_LEVEL = 'error'; -dotenv.config(); - const COMMENT_MARKER = ''; /** diff --git a/branch-suggestion/scripts/github/github-api.js b/branch-suggestion/scripts/github/github-api.js index f063993..5b7a093 100644 --- a/branch-suggestion/scripts/github/github-api.js +++ b/branch-suggestion/scripts/github/github-api.js @@ -1,7 +1,4 @@ #!/usr/bin/env node -import dotenv from 'dotenv'; - -dotenv.config(); // Configuration const GITHUB_TOKEN = process.env.GITHUB_TOKEN; diff --git a/branch-suggestion/scripts/jira/get-fixedversion.js b/branch-suggestion/scripts/jira/get-fixedversion.js index 99413ac..7c58f2e 100644 --- a/branch-suggestion/scripts/jira/get-fixedversion.js +++ b/branch-suggestion/scripts/jira/get-fixedversion.js @@ -1,14 +1,7 @@ #!/usr/bin/env node -import dotenv from 'dotenv'; import { jiraAPI, getIssue } from './jira-api.js'; // Only load .env if JIRA_TOKEN is not already set (to avoid log output in CI) -// Silence dotenv v17+ logging -if (!process.env.JIRA_TOKEN) { - process.env.DOTENV_LOG_LEVEL = 'error'; - dotenv.config(); -} - /** * Extract JIRA ticket key from text (e.g., PR title, branch name) * @param {string} text - Text to search diff --git a/branch-suggestion/scripts/jira/jira-api.js b/branch-suggestion/scripts/jira/jira-api.js index 3a17b9d..7c71749 100755 --- a/branch-suggestion/scripts/jira/jira-api.js +++ b/branch-suggestion/scripts/jira/jira-api.js @@ -1,13 +1,8 @@ #!/usr/bin/env node -import dotenv from 'dotenv'; import { URL } from 'url'; import readline from 'readline'; // Only load .env if JIRA_TOKEN is not already set (to avoid log output in CI) -if (!process.env.JIRA_TOKEN) { - dotenv.config(); -} - // JIRA configuration const JIRA_BASE_URL = 'https://tyktech.atlassian.net'; // We read JIRA_TOKEN dynamically inside jiraAPI to allow testing From f7c1aedbf419ce2bc6b5271439cd5eb2613b000c Mon Sep 17 00:00:00 2001 From: Leonid Bugaev Date: Fri, 1 May 2026 17:09:10 +0000 Subject: [PATCH 08/18] docs: remove dotenv references and add Node.js 20 --env-file info --- branch-suggestion/README.md | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/branch-suggestion/README.md b/branch-suggestion/README.md index 6101e49..4231fd2 100644 --- a/branch-suggestion/README.md +++ b/branch-suggestion/README.md @@ -103,6 +103,7 @@ npm install -g @probelabs/visor export JIRA_EMAIL="your-email@example.com" export JIRA_API_TOKEN="your-jira-api-token" ``` +Alternatively, Node.js 20 has built-in support for `.env` files. You can create a `.env` file and use the `--env-file=.env` CLI flag for local testing. ### Test Individual Scripts @@ -341,12 +342,6 @@ The tool automatically adapts to different branching strategies: **Solution:** This has been fixed in the latest version by using regex-based extraction instead of JSON parsing. Update to the latest version of the tool. -### dotenv logging pollution - -**Symptom:** `[dotenv@17.2.1]` messages in output - -**Solution:** Already fixed by setting `DOTENV_LOG_LEVEL=error` before loading dotenv. Update to the latest version. - ## Output Schema The tool outputs JSON conforming to the schema in `schemas/branch-suggestion.json`: From cf473b5164a81a24b24b1042fcfd21434ce0df58 Mon Sep 17 00:00:00 2001 From: Leonid Bugaev Date: Fri, 1 May 2026 17:27:34 +0000 Subject: [PATCH 09/18] Revert "fix: conditionally checkout PR branch for branch-suggestion workflow" This reverts commit 7bd153a06359989f653c88db3c0ad3f935942a67. --- .github/workflows/branch-suggestion.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/branch-suggestion.yml b/.github/workflows/branch-suggestion.yml index 5328dea..1abe937 100644 --- a/.github/workflows/branch-suggestion.yml +++ b/.github/workflows/branch-suggestion.yml @@ -16,12 +16,13 @@ permissions: jobs: suggest-branches: runs-on: ubuntu-latest + steps: - name: Checkout github-actions repository uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: - repository: ${{ github.repository == 'TykTechnologies/github-actions' && github.event.pull_request.head.repo.full_name || 'TykTechnologies/github-actions' }} - ref: ${{ github.repository == 'TykTechnologies/github-actions' && github.event.pull_request.head.sha || 'main' }} + repository: TykTechnologies/github-actions + ref: main - name: Setup Node.js uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 From 21dc7dcddf8d1890c32c8bad7ee4aff28da53386 Mon Sep 17 00:00:00 2001 From: Leonid Bugaev Date: Fri, 1 May 2026 18:39:56 +0000 Subject: [PATCH 10/18] fix: update JIRA_TOKEN to be unencoded and require JIRA_EMAIL --- .github/workflows/branch-suggestion.yml | 6 +++++- .github/workflows/example-usage.yml.template | 1 + branch-suggestion/README.md | 6 +++--- .../scripts/jira/__tests__/jira-api.test.js | 9 +++++++-- .../scripts/jira/get-fixedversion.js | 1 - branch-suggestion/scripts/jira/jira-api.js | 17 ++++++++--------- 6 files changed, 24 insertions(+), 16 deletions(-) diff --git a/.github/workflows/branch-suggestion.yml b/.github/workflows/branch-suggestion.yml index 1abe937..50e24f3 100644 --- a/.github/workflows/branch-suggestion.yml +++ b/.github/workflows/branch-suggestion.yml @@ -7,7 +7,10 @@ on: secrets: JIRA_TOKEN: required: true - description: 'Pre-encoded JIRA token (base64(email:api_token))' + description: 'Raw JIRA API token' + JIRA_EMAIL: + required: true + description: 'JIRA account email' permissions: pull-requests: write @@ -40,6 +43,7 @@ jobs: working-directory: branch-suggestion env: JIRA_TOKEN: ${{ secrets.JIRA_TOKEN }} + JIRA_EMAIL: ${{ secrets.JIRA_EMAIL }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} PR_TITLE: ${{ github.event.pull_request.title }} PR_NUMBER: ${{ github.event.pull_request.number }} diff --git a/.github/workflows/example-usage.yml.template b/.github/workflows/example-usage.yml.template index 7ddb719..96d2e00 100644 --- a/.github/workflows/example-usage.yml.template +++ b/.github/workflows/example-usage.yml.template @@ -16,3 +16,4 @@ jobs: uses: TykTechnologies/REFINE/.github/workflows/branch-suggestion.yml@b469f63c328ddc43e222dac10487f0a19cd0add1 # main secrets: JIRA_TOKEN: ${{ secrets.JIRA_API_TOKEN }} + JIRA_EMAIL: ${{ secrets.JIRA_EMAIL }} \ No newline at end of file diff --git a/branch-suggestion/README.md b/branch-suggestion/README.md index 4231fd2..432b6c4 100644 --- a/branch-suggestion/README.md +++ b/branch-suggestion/README.md @@ -25,9 +25,9 @@ Add this workflow to your repository to enable automatic branch suggestions: ### Example Workflow Configuration -```yaml -# .github/workflows/branch-suggestion.yml -name: PR Branch Suggestions +2. Add required secrets to your repository (Settings → Secrets and variables → Actions): + - `JIRA_EMAIL`: JIRA account email + - `JIRA_API_TOKEN`: JIRA API token (generate at https://id.atlassian.com/manage-profile/security/api-tokens) on: pull_request: diff --git a/branch-suggestion/scripts/jira/__tests__/jira-api.test.js b/branch-suggestion/scripts/jira/__tests__/jira-api.test.js index d695ba2..4dd1779 100644 --- a/branch-suggestion/scripts/jira/__tests__/jira-api.test.js +++ b/branch-suggestion/scripts/jira/__tests__/jira-api.test.js @@ -2,6 +2,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; // Set env var before importing process.env.JIRA_TOKEN = 'test-token'; +process.env.JIRA_EMAIL = 'test@example.com'; import { extractJQL, jiraAPI, searchIssues, getIssue, formatIssue, main } from '../jira-api.js'; import readline from 'readline'; @@ -51,9 +52,13 @@ describe('jiraAPI', () => { process.env = originalEnv; }); - it('should throw error if JIRA_TOKEN is not set', async () => { + it('should throw error if JIRA_TOKEN or JIRA_EMAIL is not set', async () => { delete process.env.JIRA_TOKEN; - await expect(jiraAPI('/test')).rejects.toThrow('JIRA_TOKEN must be set'); + await expect(jiraAPI('/test')).rejects.toThrow('JIRA_TOKEN and JIRA_EMAIL must be set'); + + process.env.JIRA_TOKEN = 'test-token'; + delete process.env.JIRA_EMAIL; + await expect(jiraAPI('/test')).rejects.toThrow('JIRA_TOKEN and JIRA_EMAIL must be set'); }); it('should handle 401 Unauthorized error', async () => { diff --git a/branch-suggestion/scripts/jira/get-fixedversion.js b/branch-suggestion/scripts/jira/get-fixedversion.js index 7c58f2e..7328ab7 100644 --- a/branch-suggestion/scripts/jira/get-fixedversion.js +++ b/branch-suggestion/scripts/jira/get-fixedversion.js @@ -1,7 +1,6 @@ #!/usr/bin/env node import { jiraAPI, getIssue } from './jira-api.js'; -// Only load .env if JIRA_TOKEN is not already set (to avoid log output in CI) /** * Extract JIRA ticket key from text (e.g., PR title, branch name) * @param {string} text - Text to search diff --git a/branch-suggestion/scripts/jira/jira-api.js b/branch-suggestion/scripts/jira/jira-api.js index 7c71749..be72478 100755 --- a/branch-suggestion/scripts/jira/jira-api.js +++ b/branch-suggestion/scripts/jira/jira-api.js @@ -2,7 +2,6 @@ import { URL } from 'url'; import readline from 'readline'; -// Only load .env if JIRA_TOKEN is not already set (to avoid log output in CI) // JIRA configuration const JIRA_BASE_URL = 'https://tyktech.atlassian.net'; // We read JIRA_TOKEN dynamically inside jiraAPI to allow testing @@ -33,13 +32,13 @@ function extractJQL(input) { // Make JIRA API request async function jiraAPI(endpoint, options = {}) { const token = process.env.JIRA_TOKEN; - if (!token) { - throw new Error('JIRA_TOKEN must be set in .env file (pre-encoded base64(email:api_token))'); + const email = process.env.JIRA_EMAIL; + if (!token || !email) { + throw new Error('JIRA_TOKEN and JIRA_EMAIL must be set in environment variables'); } - // Token is already base64 encoded, use directly - const auth = token; - + // Token is raw API token, encode it with email + const auth = Buffer.from(`${email}:${token}`).toString('base64'); const response = await fetch(`${JIRA_BASE_URL}/rest/api/3${endpoint}`, { ...options, headers: { @@ -131,8 +130,8 @@ async function main() { console.log(' node jira-api.js "project = TT AND status != closed"'); console.log(' node jira-api.js "https://tyktech.atlassian.net/jira/software/c/projects/TT/issues/?jql=..."'); console.log('\nMake sure to set in .env:'); - console.log(' JIRA_TOKEN='); - console.log('\nNote: JIRA_TOKEN should be pre-encoded as base64(email:api_token)'); + console.log(' JIRA_TOKEN='); + console.log(' JIRA_EMAIL='); process.exit(1); } @@ -221,7 +220,7 @@ async function main() { } catch (error) { console.error('\n❌ Error:', error.message); - console.error('\nMake sure you have set JIRA_TOKEN in your .env file (pre-encoded as base64(email:api_token))'); + console.error('\nMake sure you have set JIRA_TOKEN and JIRA_EMAIL in your environment variables'); process.exit(1); } } From 3a773eac8e5bb7b09a781e30e9dbab67686c6ecc Mon Sep 17 00:00:00 2001 From: Leonid Bugaev Date: Fri, 1 May 2026 18:41:12 +0000 Subject: [PATCH 11/18] fix: update JIRA_TOKEN to be unencoded and require JIRA_EMAIL --- branch-suggestion/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/branch-suggestion/README.md b/branch-suggestion/README.md index 432b6c4..37fff3c 100644 --- a/branch-suggestion/README.md +++ b/branch-suggestion/README.md @@ -19,6 +19,7 @@ Add this workflow to your repository to enable automatic branch suggestions: 1. Copy `.github/workflows/example-usage.yml.template` to your repository as `.github/workflows/branch-suggestion.yml` 2. Add required secrets to your repository (Settings → Secrets and variables → Actions): + - `JIRA_EMAIL`: JIRA account email - `JIRA_API_TOKEN`: JIRA API token (generate at https://id.atlassian.com/manage-profile/security/api-tokens) 3. That's it! The workflow will automatically analyze PRs and post branch suggestions. From e5092db13415c92eb1e28f5f5edf973796b6afec Mon Sep 17 00:00:00 2001 From: Leonid Bugaev Date: Fri, 1 May 2026 18:51:04 +0000 Subject: [PATCH 12/18] refactor: unify JIRA_API_TOKEN to JIRA_TOKEN --- .github/workflows/example-usage.yml.template | 2 +- branch-suggestion/README.md | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/example-usage.yml.template b/.github/workflows/example-usage.yml.template index 96d2e00..f2141fc 100644 --- a/.github/workflows/example-usage.yml.template +++ b/.github/workflows/example-usage.yml.template @@ -15,5 +15,5 @@ jobs: branch-suggestions: uses: TykTechnologies/REFINE/.github/workflows/branch-suggestion.yml@b469f63c328ddc43e222dac10487f0a19cd0add1 # main secrets: - JIRA_TOKEN: ${{ secrets.JIRA_API_TOKEN }} + JIRA_TOKEN: ${{ secrets.JIRA_TOKEN }} JIRA_EMAIL: ${{ secrets.JIRA_EMAIL }} \ No newline at end of file diff --git a/branch-suggestion/README.md b/branch-suggestion/README.md index 37fff3c..eb95934 100644 --- a/branch-suggestion/README.md +++ b/branch-suggestion/README.md @@ -20,7 +20,7 @@ Add this workflow to your repository to enable automatic branch suggestions: 2. Add required secrets to your repository (Settings → Secrets and variables → Actions): - `JIRA_EMAIL`: JIRA account email - - `JIRA_API_TOKEN`: JIRA API token (generate at https://id.atlassian.com/manage-profile/security/api-tokens) + - `JIRA_TOKEN`: JIRA API token (generate at https://id.atlassian.com/manage-profile/security/api-tokens) 3. That's it! The workflow will automatically analyze PRs and post branch suggestions. @@ -28,7 +28,7 @@ Add this workflow to your repository to enable automatic branch suggestions: 2. Add required secrets to your repository (Settings → Secrets and variables → Actions): - `JIRA_EMAIL`: JIRA account email - - `JIRA_API_TOKEN`: JIRA API token (generate at https://id.atlassian.com/manage-profile/security/api-tokens) + - `JIRA_TOKEN`: JIRA API token (generate at https://id.atlassian.com/manage-profile/security/api-tokens) on: pull_request: @@ -43,7 +43,7 @@ jobs: uses: TykTechnologies/REFINE/.github/workflows/branch-suggestion.yml@main secrets: JIRA_EMAIL: ${{ secrets.JIRA_EMAIL }} - JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }} + JIRA_TOKEN: ${{ secrets.JIRA_TOKEN }} ``` ## How It Works @@ -102,7 +102,7 @@ npm install -g @probelabs/visor 3. Set up environment variables: ```bash export JIRA_EMAIL="your-email@example.com" -export JIRA_API_TOKEN="your-jira-api-token" +export JIRA_TOKEN="your-jira-api-token" ``` Alternatively, Node.js 20 has built-in support for `.env` files. You can create a `.env` file and use the `--env-file=.env` CLI flag for local testing. @@ -192,14 +192,14 @@ node scripts/github/add-pr-comment.js \ ```bash # Test with a real ticket env JIRA_EMAIL="your-email@example.com" \ - JIRA_API_TOKEN="your-token" \ + JIRA_TOKEN="your-token" \ PR_TITLE="TT-12345" \ REPOSITORY="TykTechnologies/tyk" \ visor --config branch_suggestion.yml # Test with TIB ticket (different version format) env JIRA_EMAIL="your-email@example.com" \ - JIRA_API_TOKEN="your-token" \ + JIRA_TOKEN="your-token" \ PR_TITLE="TT-5433" \ REPOSITORY="TykTechnologies/tyk-identity-broker" \ visor --config branch_suggestion.yml @@ -272,7 +272,7 @@ BRANCH SUGGESTION ANALYSIS #### Required - `JIRA_EMAIL`: JIRA account email -- `JIRA_API_TOKEN`: JIRA API token +- `JIRA_TOKEN`: JIRA API token #### Optional (for PR comment posting) - `GITHUB_TOKEN`: GitHub token (automatically provided in GitHub Actions) From fcffe60e215b8550bec737fcf58548e1f43976ed Mon Sep 17 00:00:00 2001 From: Leonid Bugaev Date: Fri, 1 May 2026 19:33:49 +0000 Subject: [PATCH 13/18] Reimplement dynamic PR branch evaluation for checkout step --- .github/workflows/branch-suggestion.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/branch-suggestion.yml b/.github/workflows/branch-suggestion.yml index 50e24f3..5ae7159 100644 --- a/.github/workflows/branch-suggestion.yml +++ b/.github/workflows/branch-suggestion.yml @@ -25,7 +25,7 @@ jobs: uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: repository: TykTechnologies/github-actions - ref: main + ref: ${{ github.repository == 'TykTechnologies/github-actions' && github.ref || 'main' }} - name: Setup Node.js uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 From c954e04c9423251a3248d73b0da7daa077907dda Mon Sep 17 00:00:00 2001 From: Leonid Bugaev Date: Fri, 1 May 2026 19:43:21 +0000 Subject: [PATCH 14/18] fix: point Visor to current ref --- .github/workflows/branch-suggestion.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/branch-suggestion.yml b/.github/workflows/branch-suggestion.yml index 5ae7159..afc5a50 100644 --- a/.github/workflows/branch-suggestion.yml +++ b/.github/workflows/branch-suggestion.yml @@ -36,6 +36,14 @@ jobs: working-directory: branch-suggestion run: npm install --ignore-scripts + - name: Point Visor to current ref + run: | + # Visor always checks out origin/HEAD in its isolated workspace. + # We need to point origin/HEAD to the commit we just checked out + # so Visor analyzes the correct code (e.g. the PR branch). + git update-ref refs/remotes/origin/visor-target $(git rev-parse HEAD) + git symbolic-ref refs/remotes/origin/HEAD refs/remotes/origin/visor-target + - name: Install Visor run: npm install -g @probelabs/visor@0.1.185 --ignore-scripts From 0e819b92dffaf9b4bb4de49fd33299e7091ac3b6 Mon Sep 17 00:00:00 2001 From: Leonid Bugaev Date: Fri, 1 May 2026 19:51:05 +0000 Subject: [PATCH 15/18] Revert 'Point Visor to current ref' and dotenv removal This reverts commits c954e04, f7c1aed, and 709d505. --- .github/workflows/branch-suggestion.yml | 8 -------- branch-suggestion/README.md | 7 ++++++- branch-suggestion/package-lock.json | 13 +++++++++++++ branch-suggestion/package.json | 1 + branch-suggestion/scripts/common/match-branches.js | 8 ++++++++ branch-suggestion/scripts/github/add-pr-comment.js | 5 +++++ branch-suggestion/scripts/github/github-api.js | 3 +++ branch-suggestion/scripts/jira/get-fixedversion.js | 7 +++++++ branch-suggestion/scripts/jira/jira-api.js | 5 +++++ 9 files changed, 48 insertions(+), 9 deletions(-) diff --git a/.github/workflows/branch-suggestion.yml b/.github/workflows/branch-suggestion.yml index afc5a50..5ae7159 100644 --- a/.github/workflows/branch-suggestion.yml +++ b/.github/workflows/branch-suggestion.yml @@ -36,14 +36,6 @@ jobs: working-directory: branch-suggestion run: npm install --ignore-scripts - - name: Point Visor to current ref - run: | - # Visor always checks out origin/HEAD in its isolated workspace. - # We need to point origin/HEAD to the commit we just checked out - # so Visor analyzes the correct code (e.g. the PR branch). - git update-ref refs/remotes/origin/visor-target $(git rev-parse HEAD) - git symbolic-ref refs/remotes/origin/HEAD refs/remotes/origin/visor-target - - name: Install Visor run: npm install -g @probelabs/visor@0.1.185 --ignore-scripts diff --git a/branch-suggestion/README.md b/branch-suggestion/README.md index eb95934..532134a 100644 --- a/branch-suggestion/README.md +++ b/branch-suggestion/README.md @@ -104,7 +104,6 @@ npm install -g @probelabs/visor export JIRA_EMAIL="your-email@example.com" export JIRA_TOKEN="your-jira-api-token" ``` -Alternatively, Node.js 20 has built-in support for `.env` files. You can create a `.env` file and use the `--env-file=.env` CLI flag for local testing. ### Test Individual Scripts @@ -343,6 +342,12 @@ The tool automatically adapts to different branching strategies: **Solution:** This has been fixed in the latest version by using regex-based extraction instead of JSON parsing. Update to the latest version of the tool. +### dotenv logging pollution + +**Symptom:** `[dotenv@17.2.1]` messages in output + +**Solution:** Already fixed by setting `DOTENV_LOG_LEVEL=error` before loading dotenv. Update to the latest version. + ## Output Schema The tool outputs JSON conforming to the schema in `schemas/branch-suggestion.json`: diff --git a/branch-suggestion/package-lock.json b/branch-suggestion/package-lock.json index fd67bc4..b73ba68 100644 --- a/branch-suggestion/package-lock.json +++ b/branch-suggestion/package-lock.json @@ -13,6 +13,7 @@ "@anthropic-ai/sdk": "^0.60.0", "@modelcontextprotocol/sdk": "^1.17.4", "commander": "^14.0.0", + "dotenv": "^17.2.1", "mcp-remote": "^0.1.18", "turndown": "^7.2.1" }, @@ -1796,6 +1797,18 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/dotenv": { + "version": "17.2.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.1.tgz", + "integrity": "sha512-kQhDYKZecqnM0fCnzI5eIv5L4cAe/iRI+HqMbO/hbRdTAeXDG+M9FjipUxNfbARuEg4iHIbhnhs78BCHNbSxEQ==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", diff --git a/branch-suggestion/package.json b/branch-suggestion/package.json index 26c4326..19cb2e6 100644 --- a/branch-suggestion/package.json +++ b/branch-suggestion/package.json @@ -22,6 +22,7 @@ "@anthropic-ai/sdk": "^0.60.0", "@modelcontextprotocol/sdk": "^1.17.4", "commander": "^14.0.0", + "dotenv": "^17.2.1", "mcp-remote": "^0.1.18", "turndown": "^7.2.1" }, diff --git a/branch-suggestion/scripts/common/match-branches.js b/branch-suggestion/scripts/common/match-branches.js index c669c65..08d14db 100644 --- a/branch-suggestion/scripts/common/match-branches.js +++ b/branch-suggestion/scripts/common/match-branches.js @@ -1,4 +1,12 @@ #!/usr/bin/env node +import dotenv from 'dotenv'; + +// Only load .env if JIRA_TOKEN is not already set (to avoid log output in CI) +// Silence dotenv v17+ logging +if (!process.env.JIRA_TOKEN) { + process.env.DOTENV_LOG_LEVEL = 'error'; + dotenv.config(); +} /** * Generate candidate branch names based on a parsed version diff --git a/branch-suggestion/scripts/github/add-pr-comment.js b/branch-suggestion/scripts/github/add-pr-comment.js index e344950..94af2bf 100644 --- a/branch-suggestion/scripts/github/add-pr-comment.js +++ b/branch-suggestion/scripts/github/add-pr-comment.js @@ -1,4 +1,5 @@ #!/usr/bin/env node +import dotenv from 'dotenv'; import fs from 'fs'; import { getPullRequest, @@ -7,6 +8,10 @@ import { findCommentByMarker } from './github-api.js'; +// Silence dotenv v17+ logging +process.env.DOTENV_LOG_LEVEL = 'error'; +dotenv.config(); + const COMMENT_MARKER = ''; /** diff --git a/branch-suggestion/scripts/github/github-api.js b/branch-suggestion/scripts/github/github-api.js index 5b7a093..f063993 100644 --- a/branch-suggestion/scripts/github/github-api.js +++ b/branch-suggestion/scripts/github/github-api.js @@ -1,4 +1,7 @@ #!/usr/bin/env node +import dotenv from 'dotenv'; + +dotenv.config(); // Configuration const GITHUB_TOKEN = process.env.GITHUB_TOKEN; diff --git a/branch-suggestion/scripts/jira/get-fixedversion.js b/branch-suggestion/scripts/jira/get-fixedversion.js index 7328ab7..bb94617 100644 --- a/branch-suggestion/scripts/jira/get-fixedversion.js +++ b/branch-suggestion/scripts/jira/get-fixedversion.js @@ -1,6 +1,13 @@ #!/usr/bin/env node +import dotenv from 'dotenv'; import { jiraAPI, getIssue } from './jira-api.js'; +// Only load .env if JIRA_TOKEN is not already set (to avoid log output in CI) +// Silence dotenv v17+ logging +if (!process.env.JIRA_TOKEN) { + process.env.DOTENV_LOG_LEVEL = 'error'; + dotenv.config(); +} /** * Extract JIRA ticket key from text (e.g., PR title, branch name) * @param {string} text - Text to search diff --git a/branch-suggestion/scripts/jira/jira-api.js b/branch-suggestion/scripts/jira/jira-api.js index be72478..9820bca 100755 --- a/branch-suggestion/scripts/jira/jira-api.js +++ b/branch-suggestion/scripts/jira/jira-api.js @@ -1,7 +1,12 @@ #!/usr/bin/env node +import dotenv from 'dotenv'; import { URL } from 'url'; import readline from 'readline'; +// Only load .env if JIRA_TOKEN is not already set (to avoid log output in CI) +if (!process.env.JIRA_TOKEN) { + dotenv.config(); +} // JIRA configuration const JIRA_BASE_URL = 'https://tyktech.atlassian.net'; // We read JIRA_TOKEN dynamically inside jiraAPI to allow testing From afa58c91e216c4fbb3bda3429ce5a82e9d83659c Mon Sep 17 00:00:00 2001 From: Leonid Bugaev Date: Fri, 1 May 2026 20:03:40 +0000 Subject: [PATCH 16/18] fix: disable Visor workspace isolation --- .github/workflows/branch-suggestion.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/branch-suggestion.yml b/.github/workflows/branch-suggestion.yml index 5ae7159..e7ef33c 100644 --- a/.github/workflows/branch-suggestion.yml +++ b/.github/workflows/branch-suggestion.yml @@ -42,6 +42,7 @@ jobs: - name: Analyze PR and suggest branches working-directory: branch-suggestion env: + VISOR_WORKSPACE_ENABLED: "false" JIRA_TOKEN: ${{ secrets.JIRA_TOKEN }} JIRA_EMAIL: ${{ secrets.JIRA_EMAIL }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 2db1d92560e19ca28387c86565ac0a8d3e614f66 Mon Sep 17 00:00:00 2001 From: Leonid Bugaev Date: Fri, 1 May 2026 20:15:53 +0000 Subject: [PATCH 17/18] fix: rename JIRA_EMAIL to JIRA_USER_EMAIL --- .github/workflows/branch-suggestion.yml | 4 ++-- .github/workflows/example-usage.yml.template | 2 +- branch-suggestion/README.md | 16 ++++++++-------- .../scripts/jira/__tests__/jira-api.test.js | 10 +++++----- branch-suggestion/scripts/jira/jira-api.js | 8 ++++---- 5 files changed, 20 insertions(+), 20 deletions(-) diff --git a/.github/workflows/branch-suggestion.yml b/.github/workflows/branch-suggestion.yml index e7ef33c..ae3148e 100644 --- a/.github/workflows/branch-suggestion.yml +++ b/.github/workflows/branch-suggestion.yml @@ -8,7 +8,7 @@ on: JIRA_TOKEN: required: true description: 'Raw JIRA API token' - JIRA_EMAIL: + JIRA_USER_EMAIL: required: true description: 'JIRA account email' @@ -44,7 +44,7 @@ jobs: env: VISOR_WORKSPACE_ENABLED: "false" JIRA_TOKEN: ${{ secrets.JIRA_TOKEN }} - JIRA_EMAIL: ${{ secrets.JIRA_EMAIL }} + JIRA_USER_EMAIL: ${{ secrets.JIRA_USER_EMAIL }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} PR_TITLE: ${{ github.event.pull_request.title }} PR_NUMBER: ${{ github.event.pull_request.number }} diff --git a/.github/workflows/example-usage.yml.template b/.github/workflows/example-usage.yml.template index f2141fc..ecb37dd 100644 --- a/.github/workflows/example-usage.yml.template +++ b/.github/workflows/example-usage.yml.template @@ -16,4 +16,4 @@ jobs: uses: TykTechnologies/REFINE/.github/workflows/branch-suggestion.yml@b469f63c328ddc43e222dac10487f0a19cd0add1 # main secrets: JIRA_TOKEN: ${{ secrets.JIRA_TOKEN }} - JIRA_EMAIL: ${{ secrets.JIRA_EMAIL }} \ No newline at end of file + JIRA_USER_EMAIL: ${{ secrets.JIRA_USER_EMAIL }} \ No newline at end of file diff --git a/branch-suggestion/README.md b/branch-suggestion/README.md index 532134a..a898a82 100644 --- a/branch-suggestion/README.md +++ b/branch-suggestion/README.md @@ -19,7 +19,7 @@ Add this workflow to your repository to enable automatic branch suggestions: 1. Copy `.github/workflows/example-usage.yml.template` to your repository as `.github/workflows/branch-suggestion.yml` 2. Add required secrets to your repository (Settings → Secrets and variables → Actions): - - `JIRA_EMAIL`: JIRA account email + - `JIRA_USER_EMAIL`: JIRA account email - `JIRA_TOKEN`: JIRA API token (generate at https://id.atlassian.com/manage-profile/security/api-tokens) 3. That's it! The workflow will automatically analyze PRs and post branch suggestions. @@ -27,7 +27,7 @@ Add this workflow to your repository to enable automatic branch suggestions: ### Example Workflow Configuration 2. Add required secrets to your repository (Settings → Secrets and variables → Actions): - - `JIRA_EMAIL`: JIRA account email + - `JIRA_USER_EMAIL`: JIRA account email - `JIRA_TOKEN`: JIRA API token (generate at https://id.atlassian.com/manage-profile/security/api-tokens) on: @@ -42,7 +42,7 @@ jobs: branch-suggestions: uses: TykTechnologies/REFINE/.github/workflows/branch-suggestion.yml@main secrets: - JIRA_EMAIL: ${{ secrets.JIRA_EMAIL }} + JIRA_USER_EMAIL: ${{ secrets.JIRA_USER_EMAIL }} JIRA_TOKEN: ${{ secrets.JIRA_TOKEN }} ``` @@ -101,7 +101,7 @@ npm install -g @probelabs/visor 3. Set up environment variables: ```bash -export JIRA_EMAIL="your-email@example.com" +export JIRA_USER_EMAIL="your-email@example.com" export JIRA_TOKEN="your-jira-api-token" ``` @@ -190,14 +190,14 @@ node scripts/github/add-pr-comment.js \ ```bash # Test with a real ticket -env JIRA_EMAIL="your-email@example.com" \ +env JIRA_USER_EMAIL="your-email@example.com" \ JIRA_TOKEN="your-token" \ PR_TITLE="TT-12345" \ REPOSITORY="TykTechnologies/tyk" \ visor --config branch_suggestion.yml # Test with TIB ticket (different version format) -env JIRA_EMAIL="your-email@example.com" \ +env JIRA_USER_EMAIL="your-email@example.com" \ JIRA_TOKEN="your-token" \ PR_TITLE="TT-5433" \ REPOSITORY="TykTechnologies/tyk-identity-broker" \ @@ -270,7 +270,7 @@ BRANCH SUGGESTION ANALYSIS ### Environment Variables #### Required -- `JIRA_EMAIL`: JIRA account email +- `JIRA_USER_EMAIL`: JIRA account email - `JIRA_TOKEN`: JIRA API token #### Optional (for PR comment posting) @@ -323,7 +323,7 @@ The tool automatically adapts to different branching strategies: **Symptom:** Error message about authentication **Solution:** -1. Verify `JIRA_EMAIL` matches your JIRA account email +1. Verify `JIRA_USER_EMAIL` matches your JIRA account email 2. Generate a new API token at https://id.atlassian.com/manage-profile/security/api-tokens 3. Ensure the token has not expired diff --git a/branch-suggestion/scripts/jira/__tests__/jira-api.test.js b/branch-suggestion/scripts/jira/__tests__/jira-api.test.js index 4dd1779..6199369 100644 --- a/branch-suggestion/scripts/jira/__tests__/jira-api.test.js +++ b/branch-suggestion/scripts/jira/__tests__/jira-api.test.js @@ -2,7 +2,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; // Set env var before importing process.env.JIRA_TOKEN = 'test-token'; -process.env.JIRA_EMAIL = 'test@example.com'; +process.env.JIRA_USER_EMAIL = 'test@example.com'; import { extractJQL, jiraAPI, searchIssues, getIssue, formatIssue, main } from '../jira-api.js'; import readline from 'readline'; @@ -52,13 +52,13 @@ describe('jiraAPI', () => { process.env = originalEnv; }); - it('should throw error if JIRA_TOKEN or JIRA_EMAIL is not set', async () => { + it('should throw error if JIRA_TOKEN or JIRA_USER_EMAIL is not set', async () => { delete process.env.JIRA_TOKEN; - await expect(jiraAPI('/test')).rejects.toThrow('JIRA_TOKEN and JIRA_EMAIL must be set'); + await expect(jiraAPI('/test')).rejects.toThrow('JIRA_TOKEN and JIRA_USER_EMAIL must be set'); process.env.JIRA_TOKEN = 'test-token'; - delete process.env.JIRA_EMAIL; - await expect(jiraAPI('/test')).rejects.toThrow('JIRA_TOKEN and JIRA_EMAIL must be set'); + delete process.env.JIRA_USER_EMAIL; + await expect(jiraAPI('/test')).rejects.toThrow('JIRA_TOKEN and JIRA_USER_EMAIL must be set'); }); it('should handle 401 Unauthorized error', async () => { diff --git a/branch-suggestion/scripts/jira/jira-api.js b/branch-suggestion/scripts/jira/jira-api.js index 9820bca..a749a0d 100755 --- a/branch-suggestion/scripts/jira/jira-api.js +++ b/branch-suggestion/scripts/jira/jira-api.js @@ -37,9 +37,9 @@ function extractJQL(input) { // Make JIRA API request async function jiraAPI(endpoint, options = {}) { const token = process.env.JIRA_TOKEN; - const email = process.env.JIRA_EMAIL; + const email = process.env.JIRA_USER_EMAIL; if (!token || !email) { - throw new Error('JIRA_TOKEN and JIRA_EMAIL must be set in environment variables'); + throw new Error('JIRA_TOKEN and JIRA_USER_EMAIL must be set in environment variables'); } // Token is raw API token, encode it with email @@ -136,7 +136,7 @@ async function main() { console.log(' node jira-api.js "https://tyktech.atlassian.net/jira/software/c/projects/TT/issues/?jql=..."'); console.log('\nMake sure to set in .env:'); console.log(' JIRA_TOKEN='); - console.log(' JIRA_EMAIL='); + console.log(' JIRA_USER_EMAIL='); process.exit(1); } @@ -225,7 +225,7 @@ async function main() { } catch (error) { console.error('\n❌ Error:', error.message); - console.error('\nMake sure you have set JIRA_TOKEN and JIRA_EMAIL in your environment variables'); + console.error('\nMake sure you have set JIRA_TOKEN and JIRA_USER_EMAIL in your environment variables'); process.exit(1); } } From 33ac61286cc6f09b8d41aac19712f95e898255c0 Mon Sep 17 00:00:00 2001 From: Leonid Bugaev Date: Fri, 1 May 2026 20:21:20 +0000 Subject: [PATCH 18/18] fix: update timeout values in branch_suggestion.yml to milliseconds --- branch-suggestion/branch_suggestion.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/branch-suggestion/branch_suggestion.yml b/branch-suggestion/branch_suggestion.yml index 5689df7..b6f8262 100644 --- a/branch-suggestion/branch_suggestion.yml +++ b/branch-suggestion/branch_suggestion.yml @@ -16,7 +16,7 @@ checks: analyze-and-suggest: type: command tags: ["remote"] - timeout: 90 + timeout: 90000 exec: | set -e @@ -80,7 +80,7 @@ checks: type: command depends_on: [analyze-and-suggest] tags: ["remote"] - timeout: 30 + timeout: 30000 exec: | set -e