Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
e4434f1
fix(branch-suggestion): output critical JSON issue on JIRA error and …
buger May 1, 2026
7bd153a
fix: conditionally checkout PR branch for branch-suggestion workflow
buger May 1, 2026
cd04172
fix: add tags: ["remote"] to analyze-and-suggest and revert test file…
buger May 1, 2026
e8a756b
fix(branch-suggestion): separate JIRA API errors from missing fix ver…
buger May 1, 2026
19b1c71
test: add unit tests for getFixVersions and exit code logic
buger May 1, 2026
6b8005a
test: add missing tests for jira-api.js and get-fixedversion.js
buger May 1, 2026
709d505
chore: remove dotenv dependency from branch-suggestion
buger May 1, 2026
f7c1aed
docs: remove dotenv references and add Node.js 20 --env-file info
buger May 1, 2026
cf473b5
Revert "fix: conditionally checkout PR branch for branch-suggestion w…
buger May 1, 2026
21dc7dc
fix: update JIRA_TOKEN to be unencoded and require JIRA_EMAIL
buger May 1, 2026
3a773ea
fix: update JIRA_TOKEN to be unencoded and require JIRA_EMAIL
buger May 1, 2026
e5092db
refactor: unify JIRA_API_TOKEN to JIRA_TOKEN
buger May 1, 2026
fcffe60
Reimplement dynamic PR branch evaluation for checkout step
buger May 1, 2026
c954e04
fix: point Visor to current ref
buger May 1, 2026
0e819b9
Revert 'Point Visor to current ref' and dotenv removal
buger May 1, 2026
afa58c9
fix: disable Visor workspace isolation
buger May 1, 2026
2db1d92
fix: rename JIRA_EMAIL to JIRA_USER_EMAIL
buger May 1, 2026
33ac612
fix: update timeout values in branch_suggestion.yml to milliseconds
buger May 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions .github/workflows/branch-suggestion.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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_USER_EMAIL:
required: true
description: 'JIRA account email'

permissions:
pull-requests: write
Expand All @@ -22,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
Expand All @@ -39,7 +42,9 @@ jobs:
- name: Analyze PR and suggest branches
working-directory: branch-suggestion
env:
VISOR_WORKSPACE_ENABLED: "false"
JIRA_TOKEN: ${{ secrets.JIRA_TOKEN }}
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 }}
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/example-usage.yml.template
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +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_USER_EMAIL: ${{ secrets.JIRA_USER_EMAIL }}
31 changes: 16 additions & 15 deletions branch-suggestion/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,16 @@ 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_API_TOKEN`: JIRA API token (generate at https://id.atlassian.com/manage-profile/security/api-tokens)
- `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.

### 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_USER_EMAIL`: JIRA account email
- `JIRA_TOKEN`: JIRA API token (generate at https://id.atlassian.com/manage-profile/security/api-tokens)

on:
pull_request:
Expand All @@ -41,8 +42,8 @@ jobs:
branch-suggestions:
uses: TykTechnologies/REFINE/.github/workflows/branch-suggestion.yml@main
secrets:
JIRA_EMAIL: ${{ secrets.JIRA_EMAIL }}
JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }}
JIRA_USER_EMAIL: ${{ secrets.JIRA_USER_EMAIL }}
JIRA_TOKEN: ${{ secrets.JIRA_TOKEN }}
```

## How It Works
Expand Down Expand Up @@ -100,8 +101,8 @@ 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_USER_EMAIL="your-email@example.com"
export JIRA_TOKEN="your-jira-api-token"
```

### Test Individual Scripts
Expand Down Expand Up @@ -189,15 +190,15 @@ 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" \
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" \
JIRA_API_TOKEN="your-token" \
env JIRA_USER_EMAIL="your-email@example.com" \
JIRA_TOKEN="your-token" \
PR_TITLE="TT-5433" \
REPOSITORY="TykTechnologies/tyk-identity-broker" \
visor --config branch_suggestion.yml
Expand Down Expand Up @@ -269,8 +270,8 @@ BRANCH SUGGESTION ANALYSIS
### Environment Variables

#### Required
- `JIRA_EMAIL`: JIRA account email
- `JIRA_API_TOKEN`: JIRA API token
- `JIRA_USER_EMAIL`: JIRA account email
- `JIRA_TOKEN`: JIRA API token

#### Optional (for PR comment posting)
- `GITHUB_TOKEN`: GitHub token (automatically provided in GitHub Actions)
Expand Down Expand Up @@ -322,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

Expand Down
19 changes: 11 additions & 8 deletions branch-suggestion/branch_suggestion.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,32 +13,35 @@
# 2. Fetch fix versions from JIRA
# 3. Fetch repository branches
# 4. Match and suggest branches
# ============================================================================
analyze-and-suggest:
type: command
timeout: 90
tags: ["remote"]
timeout: 90000
exec: |
set -e

# Read from environment variables with defaults for local testing
PR_TITLE="${PR_TITLE:-TT-12345: Test PR}"
BRANCH_NAME="${BRANCH_NAME:-feature/TT-12345-test}"
REPO="${REPOSITORY:-TykTechnologies/tyk}"

set +e
JIRA_RESULT=$(node scripts/jira/get-fixedversion.js "$PR_TITLE" "$BRANCH_NAME")
JIRA_EXIT_CODE=$?
set -e

Check warning on line 31 in branch-suggestion/branch_suggestion.yml

View check run for this annotation

probelabs / Visor: architecture

architecture Issue

JSON issue strings are constructed directly using `echo` within the shell script. While this works for simple, static JSON, this approach is brittle and prone to syntax errors if the JSON structure becomes more complex. A typo, such as a missing quote or comma, would result in invalid JSON output that could be misinterpreted by downstream tooling.
Raw output
To improve robustness and maintainability, consider using a command-line JSON processor like `jq` to construct the JSON object. This ensures syntactically correct output and makes the intent clearer than embedding a JSON string in a shell command.

# Handle exit codes
if [ $JIRA_EXIT_CODE -eq 2 ]; then
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 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

# Fetch repository branches
Expand Down Expand Up @@ -77,7 +80,7 @@
type: command
depends_on: [analyze-and-suggest]
tags: ["remote"]
timeout: 30
timeout: 30000
exec: |
set -e

Expand All @@ -87,8 +90,8 @@

# 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
Expand Down
203 changes: 201 additions & 2 deletions branch-suggestion/scripts/jira/__tests__/get-fixedversion.test.js
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand Down Expand Up @@ -145,3 +150,197 @@ 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();
});

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();
});
});
Loading
Loading