Skip to content

Commit 7962cbd

Browse files
committed
TT-17162: support scoped tokens for branch suggestion
1 parent 1570a18 commit 7962cbd

8 files changed

Lines changed: 141 additions & 47 deletions

File tree

.github/workflows/branch-suggestion.yml

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,12 @@ on:
55
types: [opened, synchronize, reopened, ready_for_review]
66
workflow_call:
77
secrets:
8-
JIRA_TOKEN:
8+
JIRA_READ_AUTH:
99
required: true
10-
description: 'Base64 encoded JIRA API token'
10+
description: 'Base64 encoded Jira email and scoped API token'
11+
JIRA_BASE_URL:
12+
required: true
13+
description: 'Scoped Jira API base URL, e.g. https://api.atlassian.com/ex/jira/<cloudId>'
1114

1215
permissions:
1316
pull-requests: write
@@ -40,7 +43,8 @@ jobs:
4043
working-directory: branch-suggestion
4144
env:
4245
VISOR_WORKSPACE_ENABLED: "false"
43-
JIRA_TOKEN: ${{ secrets.JIRA_TOKEN }}
46+
JIRA_READ_AUTH: ${{ secrets.JIRA_READ_AUTH }}
47+
JIRA_BASE_URL: ${{ secrets.JIRA_BASE_URL }}
4448
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
4549
PR_TITLE: ${{ github.event.pull_request.title }}
4650
PR_NUMBER: ${{ github.event.pull_request.number }}

.github/workflows/example-usage.yml.template

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,7 @@ jobs:
1515
branch-suggestions:
1616
uses: TykTechnologies/REFINE/.github/workflows/branch-suggestion.yml@b469f63c328ddc43e222dac10487f0a19cd0add1 # main
1717
secrets:
18-
JIRA_TOKEN: ${{ secrets.JIRA_TOKEN }}
18+
# JIRA_READ_AUTH should be base64-encoded from "your-email@example.com:your-scoped-jira-api-token".
19+
JIRA_READ_AUTH: ${{ secrets.JIRA_READ_AUTH }}
20+
# JIRA_BASE_URL should be "https://api.atlassian.com/ex/jira/<cloudId>".
21+
JIRA_BASE_URL: ${{ secrets.JIRA_BASE_URL }}

branch-suggestion/README.md

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ Add this workflow to your repository to enable automatic branch suggestions:
1919
1. Copy `.github/workflows/example-usage.yml.template` to your repository as `.github/workflows/branch-suggestion.yml`
2020

2121
2. Add required secrets to your repository (Settings → Secrets and variables → Actions):
22-
- `JIRA_TOKEN`: Base64 encoded JIRA API token
22+
- `JIRA_READ_AUTH`: Base64 encoded Jira account email and scoped API token, generated from `email:api_token`
23+
- `JIRA_BASE_URL`: Scoped Jira API base URL, for example `https://api.atlassian.com/ex/jira/<cloudId>`
2324
3. That's it! The workflow will automatically analyze PRs and post branch suggestions.
2425

2526
### Example Workflow Configuration
@@ -37,7 +38,8 @@ jobs:
3738
branch-suggestions:
3839
uses: TykTechnologies/REFINE/.github/workflows/branch-suggestion.yml@main
3940
secrets:
40-
JIRA_TOKEN: ${{ secrets.JIRA_TOKEN }}
41+
JIRA_READ_AUTH: ${{ secrets.JIRA_READ_AUTH }}
42+
JIRA_BASE_URL: ${{ secrets.JIRA_BASE_URL }}
4143
```
4244
4345
## How It Works
@@ -95,7 +97,8 @@ npm install -g @probelabs/visor
9597

9698
3. Set up environment variables:
9799
```bash
98-
export JIRA_TOKEN="your-jira-api-token"
100+
export JIRA_READ_AUTH="$(printf '%s' 'your-email@example.com:your-scoped-jira-api-token' | base64)"
101+
export JIRA_BASE_URL="https://api.atlassian.com/ex/jira/your-cloud-id"
99102
```
100103

101104
### Test Individual Scripts
@@ -138,8 +141,9 @@ node scripts/jira/get-fixedversion.js "feature/TT-12345-fix-auth"
138141

139142
**Exit codes:**
140143
- `0`: Success (fix versions found)
141-
- `1`: Error (ticket found but no fix versions set)
144+
- `1`: Error (JIRA API or configuration error)
142145
- `2`: No JIRA ticket found
146+
- `3`: Ticket found but no fix versions set
143147

144148
#### Test Branch Matcher
145149

@@ -183,13 +187,15 @@ node scripts/github/add-pr-comment.js \
183187

184188
```bash
185189
# Test with a real ticket
186-
env JIRA_TOKEN="your-token" \
190+
env JIRA_READ_AUTH="$(printf '%s' 'your-email@example.com:your-scoped-jira-api-token' | base64)" \
191+
JIRA_BASE_URL="https://api.atlassian.com/ex/jira/your-cloud-id" \
187192
PR_TITLE="TT-12345" \
188193
REPOSITORY="TykTechnologies/tyk" \
189194
visor --config branch_suggestion.yml
190195
191196
# Test with TIB ticket (different version format)
192-
env JIRA_TOKEN="your-token" \
197+
env JIRA_READ_AUTH="$(printf '%s' 'your-email@example.com:your-scoped-jira-api-token' | base64)" \
198+
JIRA_BASE_URL="https://api.atlassian.com/ex/jira/your-cloud-id" \
193199
PR_TITLE="TT-5433" \
194200
REPOSITORY="TykTechnologies/tyk-identity-broker" \
195201
visor --config branch_suggestion.yml
@@ -261,7 +267,8 @@ BRANCH SUGGESTION ANALYSIS
261267
### Environment Variables
262268
263269
#### Required
264-
- `JIRA_TOKEN`: Base64 encoded JIRA API token
270+
- `JIRA_READ_AUTH`: Base64 encoded Jira account email and scoped API token, generated from `email:api_token`
271+
- `JIRA_BASE_URL`: Scoped Jira API base URL, for example `https://api.atlassian.com/ex/jira/<cloudId>`
265272
266273
#### Optional (for PR comment posting)
267274
- `GITHUB_TOKEN`: GitHub token (automatically provided in GitHub Actions)
@@ -313,9 +320,11 @@ The tool automatically adapts to different branching strategies:
313320
**Symptom:** Error message about authentication
314321
315322
**Solution:**
316-
1. Generate a new API token at https://id.atlassian.com/manage-profile/security/api-tokens
317-
2. Base64 encode the token with your email (e.g., `echo -n "email:token" | base64`)
318-
3. Ensure the token has not expired
323+
1. Generate a scoped API token at https://id.atlassian.com/manage-profile/security/api-tokens
324+
2. Ensure the token has Jira read scope, such as `read:jira-work`
325+
3. Base64 encode the token with your email (for example, `printf '%s' 'email:token' | base64`)
326+
4. Set `JIRA_BASE_URL` to the scoped Jira API base URL
327+
5. Ensure the token owner can browse the Jira project
319328
320329
### GitHub API rate limiting
321330

branch-suggestion/branch_suggestion.yml

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,18 @@ checks:
2525
BRANCH_NAME="${BRANCH_NAME:-feature/TT-12345-test}"
2626
REPO="${REPOSITORY:-TykTechnologies/tyk}"
2727
28+
# Visor can execute this check from a fresh temporary worktree.
29+
# Install dependencies there when node_modules is not present.
30+
if [ ! -d node_modules ]; then
31+
npm ci --ignore-scripts
32+
fi
33+
2834
set +e
29-
JIRA_RESULT=$(node scripts/jira/get-fixedversion.js "$PR_TITLE" "$BRANCH_NAME")
35+
JIRA_ERROR_FILE=$(mktemp)
36+
JIRA_RESULT=$(node scripts/jira/get-fixedversion.js "$PR_TITLE" "$BRANCH_NAME" 2>"$JIRA_ERROR_FILE")
3037
JIRA_EXIT_CODE=$?
38+
JIRA_ERROR=$(cat "$JIRA_ERROR_FILE")
39+
rm -f "$JIRA_ERROR_FILE"
3140
set -e
3241
3342
# Handle exit codes
@@ -36,7 +45,14 @@ checks:
3645
exit 0
3746
fi
3847
if [ $JIRA_EXIT_CODE -eq 1 ]; then
39-
echo '[{"file": "branch-suggestion", "line": 0, "message": "JIRA API or configuration error (e.g. invalid token).", "severity": "critical", "ruleId": "command/execution_error"}]'
48+
MESSAGE=$(printf 'JIRA API or configuration error: %s' "${JIRA_ERROR:-Unknown error}")
49+
jq -n --arg message "$MESSAGE" '[{
50+
file: "branch-suggestion",
51+
line: 0,
52+
message: $message,
53+
severity: "critical",
54+
ruleId: "command/execution_error"
55+
}]'
4056
exit 0
4157
fi
4258
if [ $JIRA_EXIT_CODE -eq 3 ]; then

branch-suggestion/scripts/common/match-branches.js

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
#!/usr/bin/env node
22
import dotenv from 'dotenv';
33

4-
// Only load .env if JIRA_TOKEN is not already set (to avoid log output in CI)
5-
// Silence dotenv v17+ logging
6-
if (!process.env.JIRA_TOKEN) {
7-
process.env.DOTENV_LOG_LEVEL = 'error';
8-
dotenv.config();
4+
// Keep local .env fallback for manual usage, but do not let dotenv write to stdout.
5+
if (!process.env.JIRA_READ_AUTH && !process.env.JIRA_TOKEN) {
6+
dotenv.config({ quiet: true });
97
}
108

119
/**
@@ -346,4 +344,3 @@ export {
346344
if (import.meta.url === `file://${process.argv[1]}`) {
347345
main();
348346
}
349-

branch-suggestion/scripts/jira/__tests__/jira-api.test.js

Lines changed: 60 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
22

33
// Set env var before importing
4-
process.env.JIRA_TOKEN = 'test-token';
4+
process.env.JIRA_READ_AUTH = 'test-token';
5+
process.env.JIRA_BASE_URL = 'https://api.atlassian.com/ex/jira/cloud-123';
56

6-
import { extractJQL, jiraAPI, searchIssues, getIssue, formatIssue, main } from '../jira-api.js';
7+
import { extractJQL, jiraAPI, searchIssues, getIssue, formatIssue, getJiraApiBaseUrl, main } from '../jira-api.js';
78
import readline from 'readline';
89

910
// Mock fetch
@@ -51,9 +52,9 @@ describe('jiraAPI', () => {
5152
process.env = originalEnv;
5253
});
5354

54-
it('should throw error if JIRA_TOKEN is not set', async () => {
55-
delete process.env.JIRA_TOKEN;
56-
await expect(jiraAPI('/test')).rejects.toThrow('JIRA_TOKEN must be set');
55+
it('should throw error if JIRA_READ_AUTH is not set', async () => {
56+
delete process.env.JIRA_READ_AUTH;
57+
await expect(jiraAPI('/test')).rejects.toThrow('JIRA_READ_AUTH must be set');
5758
});
5859

5960
it('should handle 401 Unauthorized error', async () => {
@@ -86,11 +87,44 @@ describe('jiraAPI', () => {
8687
const result = await jiraAPI('/test');
8788
expect(result).toEqual(mockData);
8889
});
90+
91+
it('should throw error if JIRA_BASE_URL is not set', async () => {
92+
delete process.env.JIRA_BASE_URL;
93+
global.fetch.mockResolvedValue({
94+
ok: true,
95+
json: vi.fn().mockResolvedValue({})
96+
});
97+
98+
await expect(jiraAPI('/test')).rejects.toThrow('JIRA_BASE_URL must be set');
99+
expect(global.fetch).not.toHaveBeenCalled();
100+
});
101+
102+
it('should use JIRA_BASE_URL for scoped API token requests', async () => {
103+
global.fetch.mockResolvedValue({
104+
ok: true,
105+
json: vi.fn().mockResolvedValue({})
106+
});
107+
108+
await jiraAPI('/test');
109+
110+
expect(global.fetch.mock.calls[0][0]).toBe('https://api.atlassian.com/ex/jira/cloud-123/rest/api/3/test');
111+
});
112+
113+
it('should remove trailing slash from JIRA_BASE_URL', () => {
114+
process.env.JIRA_BASE_URL = 'https://api.atlassian.com/ex/jira/cloud-123/';
115+
expect(getJiraApiBaseUrl()).toBe('https://api.atlassian.com/ex/jira/cloud-123');
116+
});
117+
118+
it('should require JIRA_BASE_URL when building Jira API base URL', () => {
119+
delete process.env.JIRA_BASE_URL;
120+
expect(() => getJiraApiBaseUrl()).toThrow('JIRA_BASE_URL must be set');
121+
});
89122
});
90123

91124
describe('searchIssues', () => {
92125
beforeEach(() => {
93126
global.fetch.mockClear();
127+
process.env.JIRA_BASE_URL = 'https://api.atlassian.com/ex/jira/cloud-123';
94128
});
95129

96130
it('should call jiraAPI with correct parameters', async () => {
@@ -110,6 +144,26 @@ describe('searchIssues', () => {
110144
});
111145
});
112146

147+
describe('getIssue', () => {
148+
beforeEach(() => {
149+
global.fetch.mockClear();
150+
process.env.JIRA_BASE_URL = 'https://api.atlassian.com/ex/jira/cloud-123';
151+
});
152+
153+
it('should request only fields needed for branch suggestions', async () => {
154+
global.fetch.mockResolvedValue({
155+
ok: true,
156+
json: vi.fn().mockResolvedValue({ key: 'TT-123' })
157+
});
158+
159+
await getIssue('TT-123');
160+
161+
const url = global.fetch.mock.calls[0][0];
162+
expect(url).toContain('/issue/TT-123?');
163+
expect(url).toContain('fields=summary,priority,issuetype,fixVersions');
164+
});
165+
});
166+
113167
describe('formatIssue', () => {
114168
it('should format issue correctly', () => {
115169
const issue = {
@@ -243,4 +297,4 @@ describe('main execution', () => {
243297
expect(global.fetch).toHaveBeenCalledTimes(1);
244298
expect(consoleLogMock).toHaveBeenCalledWith(expect.stringContaining('Displayed 50 of 100 total issues'));
245299
});
246-
});
300+
});

branch-suggestion/scripts/jira/get-fixedversion.js

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
import dotenv from 'dotenv';
33
import { jiraAPI, getIssue } from './jira-api.js';
44

5-
// Only load .env if JIRA_TOKEN is not already set (to avoid log output in CI)
5+
// Only load .env if auth is not already set (to avoid log output in CI)
66
// Silence dotenv v17+ logging
7-
if (!process.env.JIRA_TOKEN) {
7+
if (!process.env.JIRA_READ_AUTH) {
88
process.env.DOTENV_LOG_LEVEL = 'error';
99
dotenv.config();
1010
}
@@ -214,4 +214,3 @@ export {
214214
if (import.meta.url === `file://${process.argv[1]}`) {
215215
main();
216216
}
217-

0 commit comments

Comments
 (0)