semantic-release-linear-app 0.3.0 → 0.4.0-next.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -4
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -7
- package/dist/lib/context.d.ts +1 -1
- package/dist/lib/context.js +2 -6
- package/dist/lib/linear-client.d.ts +1 -1
- package/dist/lib/linear-client.js +3 -10
- package/dist/lib/parse-issues.js +1 -4
- package/dist/lib/parse-issues.test.js +6 -7
- package/dist/lib/success.d.ts +1 -1
- package/dist/lib/success.js +9 -12
- package/dist/lib/verify.d.ts +1 -1
- package/dist/lib/verify.js +14 -20
- package/dist/lib/verify.test.js +15 -16
- package/dist/types.d.ts +2 -2
- package/dist/types.js +1 -2
- package/package.json +23 -7
- package/src/index.ts +3 -3
- package/src/lib/context.ts +1 -1
- package/src/lib/linear-client.ts +1 -1
- package/src/lib/parse-issues.test.ts +2 -1
- package/src/lib/success.ts +4 -4
- package/src/lib/verify.test.ts +17 -15
- package/src/lib/verify.ts +11 -11
- package/src/types.ts +2 -2
package/README.md
CHANGED
|
@@ -25,7 +25,7 @@ npm install --save-dev semantic-release-linear-app
|
|
|
25
25
|
}
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
-
2. Set `
|
|
28
|
+
2. Set `LINEAR_TOKEN` environment variable (see [Authentication](#authentication) below)
|
|
29
29
|
|
|
30
30
|
3. Use branch names with Linear issue IDs:
|
|
31
31
|
|
|
@@ -45,6 +45,29 @@ ENG-789
|
|
|
45
45
|
| `addComment` | `false` | Add release comment to issues |
|
|
46
46
|
| `dryRun` | `false` | Preview without making changes |
|
|
47
47
|
|
|
48
|
+
## How it Works
|
|
49
|
+
|
|
50
|
+
This plugin hooks into two semantic-release lifecycle stages:
|
|
51
|
+
|
|
52
|
+
```mermaid
|
|
53
|
+
flowchart LR
|
|
54
|
+
subgraph semantic-release
|
|
55
|
+
A[verifyConditions] --> B[analyzeCommits]
|
|
56
|
+
B --> C[generateNotes]
|
|
57
|
+
C --> D[publish]
|
|
58
|
+
D --> E[success]
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
A -. "validate Linear auth" .-> A
|
|
62
|
+
E -. "update Linear issues" .-> E
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
1. **verifyConditions** - Validates your `LINEAR_TOKEN` and tests the API connection
|
|
66
|
+
2. **success** - After release is published:
|
|
67
|
+
- Finds branches containing the release commits
|
|
68
|
+
- Extracts issue IDs from branch names (e.g., `ENG-123`)
|
|
69
|
+
- Creates/applies version label to each issue
|
|
70
|
+
|
|
48
71
|
## Labels
|
|
49
72
|
|
|
50
73
|
Labels are created based on version and channel:
|
|
@@ -54,8 +77,23 @@ Labels are created based on version and channel:
|
|
|
54
77
|
|
|
55
78
|
Colors indicate release type: red (major), orange (minor), green (patch), purple (prerelease).
|
|
56
79
|
|
|
57
|
-
##
|
|
80
|
+
## Authentication
|
|
81
|
+
|
|
82
|
+
Set `LINEAR_TOKEN` environment variable with either:
|
|
83
|
+
|
|
84
|
+
### API Key (Simple)
|
|
85
|
+
Actions appear as your personal account.
|
|
86
|
+
1. Go to Linear Settings > API > Personal API keys
|
|
87
|
+
2. Create new key
|
|
88
|
+
3. Set as `LINEAR_TOKEN`
|
|
58
89
|
|
|
59
|
-
|
|
90
|
+
### OAuth Access Token (Recommended for CI)
|
|
91
|
+
Actions appear as your app name instead of a user.
|
|
92
|
+
1. Go to Linear Settings > API > Applications
|
|
93
|
+
2. Create new application, enable "Client credentials tokens"
|
|
94
|
+
3. Use client credentials to get an access token ([docs](https://linear.app/developers/oauth-2-0-authentication))
|
|
95
|
+
4. Set the access token as `LINEAR_TOKEN`
|
|
96
|
+
|
|
97
|
+
## License
|
|
60
98
|
|
|
61
|
-
|
|
99
|
+
MIT
|
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* semantic-release-linear-app
|
|
3
3
|
* A semantic-release plugin to update Linear issues with version labels
|
|
4
4
|
*/
|
|
5
|
-
import { verifyConditions } from
|
|
6
|
-
import { success } from
|
|
5
|
+
import { verifyConditions } from './lib/verify.js';
|
|
6
|
+
import { success } from './lib/success.js';
|
|
7
7
|
export { verifyConditions, success };
|
|
8
|
-
export type { PluginConfig } from
|
|
8
|
+
export type { PluginConfig } from './types.js';
|
package/dist/index.js
CHANGED
|
@@ -1,11 +1,7 @@
|
|
|
1
|
-
"use strict";
|
|
2
1
|
/**
|
|
3
2
|
* semantic-release-linear-app
|
|
4
3
|
* A semantic-release plugin to update Linear issues with version labels
|
|
5
4
|
*/
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
Object.defineProperty(exports, "verifyConditions", { enumerable: true, get: function () { return verify_1.verifyConditions; } });
|
|
10
|
-
const success_1 = require("./lib/success");
|
|
11
|
-
Object.defineProperty(exports, "success", { enumerable: true, get: function () { return success_1.success; } });
|
|
5
|
+
import { verifyConditions } from './lib/verify.js';
|
|
6
|
+
import { success } from './lib/success.js';
|
|
7
|
+
export { verifyConditions, success };
|
package/dist/lib/context.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { LinearContext } from '../types';
|
|
1
|
+
import { LinearContext } from '../types.js';
|
|
2
2
|
/** Store Linear context for access across hooks */
|
|
3
3
|
export declare function setLinearContext(ctx: LinearContext): void;
|
|
4
4
|
export declare function getLinearContext(): LinearContext | null;
|
package/dist/lib/context.js
CHANGED
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.setLinearContext = setLinearContext;
|
|
4
|
-
exports.getLinearContext = getLinearContext;
|
|
5
1
|
/**
|
|
6
2
|
* Module-level storage for Linear context between semantic-release hooks.
|
|
7
3
|
* semantic-release creates separate context objects per hook, so we need
|
|
@@ -9,9 +5,9 @@ exports.getLinearContext = getLinearContext;
|
|
|
9
5
|
*/
|
|
10
6
|
let linearContext = null;
|
|
11
7
|
/** Store Linear context for access across hooks */
|
|
12
|
-
function setLinearContext(ctx) {
|
|
8
|
+
export function setLinearContext(ctx) {
|
|
13
9
|
linearContext = ctx;
|
|
14
10
|
}
|
|
15
|
-
function getLinearContext() {
|
|
11
|
+
export function getLinearContext() {
|
|
16
12
|
return linearContext;
|
|
17
13
|
}
|
|
@@ -1,14 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.LinearClient = void 0;
|
|
7
|
-
const node_fetch_1 = __importDefault(require("node-fetch"));
|
|
1
|
+
import fetch from 'node-fetch';
|
|
8
2
|
/**
|
|
9
3
|
* Linear API client for GraphQL operations
|
|
10
4
|
*/
|
|
11
|
-
class LinearClient {
|
|
5
|
+
export class LinearClient {
|
|
12
6
|
apiKey;
|
|
13
7
|
apiUrl = 'https://api.linear.app/graphql';
|
|
14
8
|
constructor(apiKey) {
|
|
@@ -18,7 +12,7 @@ class LinearClient {
|
|
|
18
12
|
* Execute a GraphQL query
|
|
19
13
|
*/
|
|
20
14
|
async query(query, variables = {}) {
|
|
21
|
-
const response = await (
|
|
15
|
+
const response = await fetch(this.apiUrl, {
|
|
22
16
|
method: 'POST',
|
|
23
17
|
headers: {
|
|
24
18
|
Authorization: this.apiKey,
|
|
@@ -192,4 +186,3 @@ class LinearClient {
|
|
|
192
186
|
return data.commentCreate.comment;
|
|
193
187
|
}
|
|
194
188
|
}
|
|
195
|
-
exports.LinearClient = LinearClient;
|
package/dist/lib/parse-issues.js
CHANGED
|
@@ -1,17 +1,14 @@
|
|
|
1
|
-
"use strict";
|
|
2
1
|
/**
|
|
3
2
|
* Extract Linear issue IDs from branch name ONLY
|
|
4
3
|
* This enforces a single source of truth for issue tracking
|
|
5
4
|
*/
|
|
6
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
-
exports.parseIssuesFromBranch = parseIssuesFromBranch;
|
|
8
5
|
/**
|
|
9
6
|
* Extract Linear issue IDs from a branch name
|
|
10
7
|
* @param branchName - The branch name to parse
|
|
11
8
|
* @param teamKeys - Optional list of team keys to filter by
|
|
12
9
|
* @returns Array of unique issue identifiers
|
|
13
10
|
*/
|
|
14
|
-
function parseIssuesFromBranch(branchName, teamKeys = null) {
|
|
11
|
+
export function parseIssuesFromBranch(branchName, teamKeys = null) {
|
|
15
12
|
const issues = new Set();
|
|
16
13
|
// Build regex pattern based on team keys
|
|
17
14
|
const teamPattern = teamKeys ? `(?:${teamKeys.join('|')})` : '[A-Z]+';
|
|
@@ -1,26 +1,25 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
const parse_issues_1 = require("./parse-issues");
|
|
1
|
+
import { describe, test, expect } from '@jest/globals';
|
|
2
|
+
import { parseIssuesFromBranch } from './parse-issues.js';
|
|
4
3
|
describe('parse-issues', () => {
|
|
5
4
|
test('extracts Linear issue IDs from branch name', () => {
|
|
6
5
|
const branchName = 'feature/ENG-123-add-new-feature';
|
|
7
|
-
const result =
|
|
6
|
+
const result = parseIssuesFromBranch(branchName);
|
|
8
7
|
expect(result).toEqual(['ENG-123']);
|
|
9
8
|
});
|
|
10
9
|
test('extracts multiple issue IDs from branch name', () => {
|
|
11
10
|
const branchName = 'fix/ENG-123-FEAT-456-bug-fix';
|
|
12
|
-
const result =
|
|
11
|
+
const result = parseIssuesFromBranch(branchName);
|
|
13
12
|
expect(result).toEqual(expect.arrayContaining(['ENG-123', 'FEAT-456']));
|
|
14
13
|
expect(result).toHaveLength(2);
|
|
15
14
|
});
|
|
16
15
|
test('filters by team keys when provided', () => {
|
|
17
16
|
const branchName = 'feature/ENG-123-OTHER-456';
|
|
18
|
-
const result =
|
|
17
|
+
const result = parseIssuesFromBranch(branchName, ['ENG']);
|
|
19
18
|
expect(result).toEqual(['ENG-123']);
|
|
20
19
|
});
|
|
21
20
|
test('returns empty array for branch without issues', () => {
|
|
22
21
|
const branchName = 'feature/no-issues-here';
|
|
23
|
-
const result =
|
|
22
|
+
const result = parseIssuesFromBranch(branchName);
|
|
24
23
|
expect(result).toEqual([]);
|
|
25
24
|
});
|
|
26
25
|
});
|
package/dist/lib/success.d.ts
CHANGED
package/dist/lib/success.js
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const linear_client_1 = require("./linear-client");
|
|
6
|
-
const parse_issues_1 = require("./parse-issues");
|
|
7
|
-
const context_1 = require("./context");
|
|
1
|
+
import { execa } from 'execa';
|
|
2
|
+
import { LinearClient } from './linear-client.js';
|
|
3
|
+
import { parseIssuesFromBranch } from './parse-issues.js';
|
|
4
|
+
import { getLinearContext } from './context.js';
|
|
8
5
|
/**
|
|
9
6
|
* Find source branches that contain the given commits
|
|
10
7
|
*/
|
|
@@ -16,7 +13,7 @@ async function findSourceBranches(commits, logger) {
|
|
|
16
13
|
// Check all commits to find all source branches
|
|
17
14
|
for (const commit of commits) {
|
|
18
15
|
try {
|
|
19
|
-
const { stdout } = await
|
|
16
|
+
const { stdout } = await execa('git', [
|
|
20
17
|
'branch',
|
|
21
18
|
'-r',
|
|
22
19
|
'--contains',
|
|
@@ -43,9 +40,9 @@ async function findSourceBranches(commits, logger) {
|
|
|
43
40
|
/**
|
|
44
41
|
* Update Linear issues after a successful release
|
|
45
42
|
*/
|
|
46
|
-
async function success(pluginConfig, context) {
|
|
43
|
+
export async function success(pluginConfig, context) {
|
|
47
44
|
const { logger, nextRelease, commits } = context;
|
|
48
|
-
const linear =
|
|
45
|
+
const linear = getLinearContext();
|
|
49
46
|
if (!linear) {
|
|
50
47
|
logger.log('Linear context not found, skipping issue updates');
|
|
51
48
|
return;
|
|
@@ -64,7 +61,7 @@ async function success(pluginConfig, context) {
|
|
|
64
61
|
// Extract Linear issue IDs from all found branches
|
|
65
62
|
const issueIds = new Set();
|
|
66
63
|
for (const branchName of Array.from(sourceBranches)) {
|
|
67
|
-
const branchIssues =
|
|
64
|
+
const branchIssues = parseIssuesFromBranch(branchName, linear.teamKeys);
|
|
68
65
|
branchIssues.forEach((id) => issueIds.add(id));
|
|
69
66
|
}
|
|
70
67
|
if (issueIds.size === 0) {
|
|
@@ -82,7 +79,7 @@ async function success(pluginConfig, context) {
|
|
|
82
79
|
return;
|
|
83
80
|
}
|
|
84
81
|
// Initialize Linear client and prepare label
|
|
85
|
-
const client = new
|
|
82
|
+
const client = new LinearClient(linear.apiKey);
|
|
86
83
|
const labelColor = getLabelColor(nextRelease.type);
|
|
87
84
|
// Ensure the version label exists
|
|
88
85
|
const label = await client.ensureLabel(labelName, labelColor);
|
package/dist/lib/verify.d.ts
CHANGED
package/dist/lib/verify.js
CHANGED
|
@@ -1,44 +1,38 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.verifyConditions = verifyConditions;
|
|
7
|
-
const error_1 = __importDefault(require("@semantic-release/error"));
|
|
8
|
-
const linear_client_1 = require("./linear-client");
|
|
9
|
-
const context_1 = require("./context");
|
|
1
|
+
import SemanticReleaseError from '@semantic-release/error';
|
|
2
|
+
import { LinearClient } from './linear-client.js';
|
|
3
|
+
import { setLinearContext } from './context.js';
|
|
10
4
|
/**
|
|
11
5
|
* Verify the plugin configuration and Linear API access
|
|
12
6
|
*/
|
|
13
|
-
async function verifyConditions(pluginConfig, context) {
|
|
7
|
+
export async function verifyConditions(pluginConfig, context) {
|
|
14
8
|
const { logger } = context;
|
|
15
|
-
const {
|
|
16
|
-
// Check for
|
|
17
|
-
const
|
|
18
|
-
if (!
|
|
19
|
-
throw new
|
|
9
|
+
const { token, teamKeys = [] } = pluginConfig;
|
|
10
|
+
// Check for token in config or environment
|
|
11
|
+
const linearToken = token || process.env.LINEAR_TOKEN;
|
|
12
|
+
if (!linearToken) {
|
|
13
|
+
throw new SemanticReleaseError('No Linear token found', 'ENOLINEARTOKEN', 'Please provide LINEAR_TOKEN environment variable.');
|
|
20
14
|
}
|
|
21
15
|
// Validate team keys format if provided
|
|
22
16
|
const teamKeyPattern = /^[A-Z]+$/;
|
|
23
17
|
const branchPattern = /^[A-Za-z0-9._-]+\/[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
24
18
|
const invalidTeamKeys = teamKeys.filter((key) => !teamKeyPattern.test(key) && !branchPattern.test(key));
|
|
25
19
|
if (invalidTeamKeys.length > 0) {
|
|
26
|
-
throw new
|
|
20
|
+
throw new SemanticReleaseError('Invalid team key format', 'EINVALIDTEAMKEY', 'Team keys must be uppercase letters (e.g. SD) or branch names (e.g. caio/tk-519-title). ' +
|
|
27
21
|
`Invalid: ${invalidTeamKeys.join(', ')}`);
|
|
28
22
|
}
|
|
29
23
|
// Test API connection
|
|
30
|
-
const client = new
|
|
24
|
+
const client = new LinearClient(linearToken);
|
|
31
25
|
try {
|
|
32
26
|
logger.log('Verifying Linear API access...');
|
|
33
27
|
await client.testConnection();
|
|
34
28
|
logger.log('✓ Linear API access verified');
|
|
35
29
|
}
|
|
36
30
|
catch (error) {
|
|
37
|
-
throw new
|
|
31
|
+
throw new SemanticReleaseError('Failed to connect to Linear API', 'ELINEARCONNECTION', `Could not connect to Linear API: ${error.message}`);
|
|
38
32
|
}
|
|
39
33
|
// Store validated config for other lifecycle methods
|
|
40
|
-
|
|
41
|
-
apiKey:
|
|
34
|
+
setLinearContext({
|
|
35
|
+
apiKey: linearToken,
|
|
42
36
|
teamKeys: teamKeys.length > 0 ? teamKeys : null,
|
|
43
37
|
labelPrefix: pluginConfig.labelPrefix || 'v',
|
|
44
38
|
});
|
package/dist/lib/verify.test.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
jest.mock('@semantic-release/error', () => {
|
|
1
|
+
import { jest, describe, test, expect, beforeEach } from '@jest/globals';
|
|
2
|
+
// ESM mocks must be set up before imports
|
|
3
|
+
jest.unstable_mockModule('@semantic-release/error', () => {
|
|
5
4
|
class SemanticReleaseError extends Error {
|
|
6
5
|
code;
|
|
7
6
|
constructor(message, code) {
|
|
@@ -9,33 +8,33 @@ jest.mock('@semantic-release/error', () => {
|
|
|
9
8
|
this.code = code;
|
|
10
9
|
}
|
|
11
10
|
}
|
|
12
|
-
return {
|
|
11
|
+
return { default: SemanticReleaseError };
|
|
13
12
|
});
|
|
14
|
-
jest.
|
|
15
|
-
LinearClient: jest.fn(
|
|
16
|
-
testConnection: jest.fn().
|
|
13
|
+
jest.unstable_mockModule('./linear-client.js', () => ({
|
|
14
|
+
LinearClient: jest.fn(() => ({
|
|
15
|
+
testConnection: jest.fn(() => Promise.resolve({})),
|
|
17
16
|
})),
|
|
18
17
|
}));
|
|
19
|
-
jest.
|
|
20
|
-
const
|
|
18
|
+
jest.unstable_mockModule('node-fetch', () => ({ default: jest.fn() }));
|
|
19
|
+
const { verifyConditions } = await import('./verify.js');
|
|
21
20
|
describe('verify', () => {
|
|
22
21
|
const mockContext = {
|
|
23
22
|
logger: { log: jest.fn(), error: jest.fn() },
|
|
24
23
|
};
|
|
25
24
|
beforeEach(() => {
|
|
26
|
-
delete process.env.
|
|
25
|
+
delete process.env.LINEAR_TOKEN;
|
|
27
26
|
});
|
|
28
|
-
test('throws without
|
|
29
|
-
await expect(
|
|
27
|
+
test('throws without token', async () => {
|
|
28
|
+
await expect(verifyConditions({}, mockContext)).rejects.toThrow('No Linear token found');
|
|
30
29
|
});
|
|
31
30
|
test('validates team key format', async () => {
|
|
32
31
|
// Validation happens BEFORE the API call, so it fails fast
|
|
33
|
-
await expect(
|
|
32
|
+
await expect(verifyConditions({ token: 'test', teamKeys: ['eng-123'] }, mockContext)).rejects.toThrow('Invalid team key format');
|
|
34
33
|
});
|
|
35
34
|
test('accepts valid branch like team key', async () => {
|
|
36
35
|
// Accepts branch patterns without hitting API
|
|
37
|
-
await expect(
|
|
38
|
-
|
|
36
|
+
await expect(verifyConditions({
|
|
37
|
+
token: 'test',
|
|
39
38
|
teamKeys: ['caio/tk-519-title'],
|
|
40
39
|
}, mockContext)).resolves.toBeUndefined();
|
|
41
40
|
});
|
package/dist/types.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export interface PluginConfig {
|
|
2
|
-
/** Linear API key (can also use
|
|
3
|
-
|
|
2
|
+
/** Linear token - API key or OAuth access token (can also use LINEAR_TOKEN env var) */
|
|
3
|
+
token?: string;
|
|
4
4
|
/** Team keys to filter issues (e.g., ["ENG", "FEAT"]) */
|
|
5
5
|
teamKeys?: string[];
|
|
6
6
|
/** Prefix for version labels (default: "v") */
|
package/dist/types.js
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "semantic-release-linear-app",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0-next.2",
|
|
4
4
|
"description": "Semantic-release plugin to update Linear issues with version labels",
|
|
5
|
+
"type": "module",
|
|
5
6
|
"main": "dist/index.js",
|
|
6
7
|
"types": "dist/index.d.ts",
|
|
7
8
|
"engines": {
|
|
@@ -17,12 +18,27 @@
|
|
|
17
18
|
"lint": "eslint src/**/*.ts",
|
|
18
19
|
"format": "prettier --write src/**/*.ts",
|
|
19
20
|
"prepare": "husky",
|
|
20
|
-
"test": "jest",
|
|
21
|
-
"test:watch": "jest --watch"
|
|
21
|
+
"test": "NODE_OPTIONS='--experimental-vm-modules' jest",
|
|
22
|
+
"test:watch": "jest --watch",
|
|
23
|
+
"validate": "pnpm lint && pnpm type-check && pnpm build && pnpm test"
|
|
22
24
|
},
|
|
23
25
|
"jest": {
|
|
24
|
-
"preset": "ts-jest",
|
|
26
|
+
"preset": "ts-jest/presets/default-esm",
|
|
25
27
|
"testEnvironment": "node",
|
|
28
|
+
"extensionsToTreatAsEsm": [
|
|
29
|
+
".ts"
|
|
30
|
+
],
|
|
31
|
+
"moduleNameMapper": {
|
|
32
|
+
"^(\\.{1,2}/.*)\\.js$": "$1"
|
|
33
|
+
},
|
|
34
|
+
"transform": {
|
|
35
|
+
"^.+\\.tsx?$": [
|
|
36
|
+
"ts-jest",
|
|
37
|
+
{
|
|
38
|
+
"useESM": true
|
|
39
|
+
}
|
|
40
|
+
]
|
|
41
|
+
},
|
|
26
42
|
"testMatch": [
|
|
27
43
|
"**/*.test.ts"
|
|
28
44
|
]
|
|
@@ -39,7 +55,7 @@
|
|
|
39
55
|
"license": "MIT",
|
|
40
56
|
"repository": {
|
|
41
57
|
"type": "git",
|
|
42
|
-
"url": "https://github.com/caiopizzol/semantic-release-linear.git"
|
|
58
|
+
"url": "https://github.com/caiopizzol/semantic-release-linear-app.git"
|
|
43
59
|
},
|
|
44
60
|
"peerDependencies": {
|
|
45
61
|
"semantic-release": ">=20.0.0"
|
|
@@ -50,6 +66,7 @@
|
|
|
50
66
|
"node-fetch": "^3.3.2"
|
|
51
67
|
},
|
|
52
68
|
"devDependencies": {
|
|
69
|
+
"@jest/globals": "^30.2.0",
|
|
53
70
|
"@semantic-release/commit-analyzer": "^13.0.1",
|
|
54
71
|
"@semantic-release/git": "^10.0.1",
|
|
55
72
|
"@semantic-release/github": "^12.0.2",
|
|
@@ -77,8 +94,7 @@
|
|
|
77
94
|
"lint-staged": {
|
|
78
95
|
"*.ts": [
|
|
79
96
|
"eslint --fix",
|
|
80
|
-
"prettier --write"
|
|
81
|
-
"tsc --noEmit"
|
|
97
|
+
"prettier --write"
|
|
82
98
|
]
|
|
83
99
|
}
|
|
84
100
|
}
|
package/src/index.ts
CHANGED
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
* A semantic-release plugin to update Linear issues with version labels
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { verifyConditions } from
|
|
7
|
-
import { success } from
|
|
6
|
+
import { verifyConditions } from './lib/verify.js';
|
|
7
|
+
import { success } from './lib/success.js';
|
|
8
8
|
|
|
9
9
|
export { verifyConditions, success };
|
|
10
10
|
|
|
11
11
|
// Also export types for consumers who use TypeScript
|
|
12
|
-
export type { PluginConfig } from
|
|
12
|
+
export type { PluginConfig } from './types.js';
|
package/src/lib/context.ts
CHANGED
package/src/lib/linear-client.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { describe, test, expect } from '@jest/globals';
|
|
2
|
+
import { parseIssuesFromBranch } from './parse-issues.js';
|
|
2
3
|
|
|
3
4
|
describe('parse-issues', () => {
|
|
4
5
|
test('extracts Linear issue IDs from branch name', () => {
|
package/src/lib/success.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { SuccessContext } from 'semantic-release';
|
|
2
2
|
import { execa } from 'execa';
|
|
3
|
-
import { LinearClient } from './linear-client';
|
|
4
|
-
import { parseIssuesFromBranch } from './parse-issues';
|
|
5
|
-
import { PluginConfig, ReleaseType } from '../types';
|
|
6
|
-
import { getLinearContext } from './context';
|
|
3
|
+
import { LinearClient } from './linear-client.js';
|
|
4
|
+
import { parseIssuesFromBranch } from './parse-issues.js';
|
|
5
|
+
import { PluginConfig, ReleaseType } from '../types.js';
|
|
6
|
+
import { getLinearContext } from './context.js';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Find source branches that contain the given commits
|
package/src/lib/verify.test.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { jest, describe, test, expect, beforeEach } from '@jest/globals';
|
|
2
|
+
|
|
3
|
+
// ESM mocks must be set up before imports
|
|
4
|
+
jest.unstable_mockModule('@semantic-release/error', () => {
|
|
3
5
|
class SemanticReleaseError extends Error {
|
|
4
6
|
code: string;
|
|
5
7
|
constructor(message: string, code: string) {
|
|
@@ -7,37 +9,37 @@ jest.mock('@semantic-release/error', () => {
|
|
|
7
9
|
this.code = code;
|
|
8
10
|
}
|
|
9
11
|
}
|
|
10
|
-
return {
|
|
12
|
+
return { default: SemanticReleaseError };
|
|
11
13
|
});
|
|
12
14
|
|
|
13
|
-
jest.
|
|
14
|
-
LinearClient: jest.fn(
|
|
15
|
-
testConnection: jest.fn().
|
|
15
|
+
jest.unstable_mockModule('./linear-client.js', () => ({
|
|
16
|
+
LinearClient: jest.fn(() => ({
|
|
17
|
+
testConnection: jest.fn(() => Promise.resolve({})),
|
|
16
18
|
})),
|
|
17
19
|
}));
|
|
18
20
|
|
|
19
|
-
jest.
|
|
21
|
+
jest.unstable_mockModule('node-fetch', () => ({ default: jest.fn() }));
|
|
20
22
|
|
|
21
|
-
|
|
22
|
-
import { VerifyConditionsContext } from 'semantic-release';
|
|
23
|
+
const { verifyConditions } = await import('./verify.js');
|
|
24
|
+
import type { VerifyConditionsContext } from 'semantic-release';
|
|
23
25
|
|
|
24
26
|
describe('verify', () => {
|
|
25
27
|
const mockContext = {
|
|
26
28
|
logger: { log: jest.fn(), error: jest.fn() },
|
|
27
|
-
} as VerifyConditionsContext;
|
|
29
|
+
} as unknown as VerifyConditionsContext;
|
|
28
30
|
|
|
29
31
|
beforeEach(() => {
|
|
30
|
-
delete process.env.
|
|
32
|
+
delete process.env.LINEAR_TOKEN;
|
|
31
33
|
});
|
|
32
34
|
|
|
33
|
-
test('throws without
|
|
34
|
-
await expect(verifyConditions({}, mockContext)).rejects.toThrow('No Linear
|
|
35
|
+
test('throws without token', async () => {
|
|
36
|
+
await expect(verifyConditions({}, mockContext)).rejects.toThrow('No Linear token found');
|
|
35
37
|
});
|
|
36
38
|
|
|
37
39
|
test('validates team key format', async () => {
|
|
38
40
|
// Validation happens BEFORE the API call, so it fails fast
|
|
39
41
|
await expect(
|
|
40
|
-
verifyConditions({
|
|
42
|
+
verifyConditions({ token: 'test', teamKeys: ['eng-123'] }, mockContext),
|
|
41
43
|
).rejects.toThrow('Invalid team key format');
|
|
42
44
|
});
|
|
43
45
|
|
|
@@ -46,7 +48,7 @@ describe('verify', () => {
|
|
|
46
48
|
await expect(
|
|
47
49
|
verifyConditions(
|
|
48
50
|
{
|
|
49
|
-
|
|
51
|
+
token: 'test',
|
|
50
52
|
teamKeys: ['caio/tk-519-title'],
|
|
51
53
|
},
|
|
52
54
|
mockContext,
|
package/src/lib/verify.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import SemanticReleaseError from '@semantic-release/error';
|
|
2
2
|
import { VerifyConditionsContext } from 'semantic-release';
|
|
3
|
-
import { LinearClient } from './linear-client';
|
|
4
|
-
import { PluginConfig } from '../types';
|
|
5
|
-
import { setLinearContext } from './context';
|
|
3
|
+
import { LinearClient } from './linear-client.js';
|
|
4
|
+
import { PluginConfig } from '../types.js';
|
|
5
|
+
import { setLinearContext } from './context.js';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Verify the plugin configuration and Linear API access
|
|
@@ -12,16 +12,16 @@ export async function verifyConditions(
|
|
|
12
12
|
context: VerifyConditionsContext,
|
|
13
13
|
): Promise<void> {
|
|
14
14
|
const { logger } = context;
|
|
15
|
-
const {
|
|
15
|
+
const { token, teamKeys = [] } = pluginConfig;
|
|
16
16
|
|
|
17
|
-
// Check for
|
|
18
|
-
const
|
|
17
|
+
// Check for token in config or environment
|
|
18
|
+
const linearToken = token || process.env.LINEAR_TOKEN;
|
|
19
19
|
|
|
20
|
-
if (!
|
|
20
|
+
if (!linearToken) {
|
|
21
21
|
throw new SemanticReleaseError(
|
|
22
|
-
'No Linear
|
|
22
|
+
'No Linear token found',
|
|
23
23
|
'ENOLINEARTOKEN',
|
|
24
|
-
'Please provide
|
|
24
|
+
'Please provide LINEAR_TOKEN environment variable.',
|
|
25
25
|
);
|
|
26
26
|
}
|
|
27
27
|
|
|
@@ -42,7 +42,7 @@ export async function verifyConditions(
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
// Test API connection
|
|
45
|
-
const client = new LinearClient(
|
|
45
|
+
const client = new LinearClient(linearToken);
|
|
46
46
|
|
|
47
47
|
try {
|
|
48
48
|
logger.log('Verifying Linear API access...');
|
|
@@ -58,7 +58,7 @@ export async function verifyConditions(
|
|
|
58
58
|
|
|
59
59
|
// Store validated config for other lifecycle methods
|
|
60
60
|
setLinearContext({
|
|
61
|
-
apiKey:
|
|
61
|
+
apiKey: linearToken,
|
|
62
62
|
teamKeys: teamKeys.length > 0 ? teamKeys : null,
|
|
63
63
|
labelPrefix: pluginConfig.labelPrefix || 'v',
|
|
64
64
|
});
|
package/src/types.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export interface PluginConfig {
|
|
2
|
-
/** Linear API key (can also use
|
|
3
|
-
|
|
2
|
+
/** Linear token - API key or OAuth access token (can also use LINEAR_TOKEN env var) */
|
|
3
|
+
token?: string;
|
|
4
4
|
|
|
5
5
|
/** Team keys to filter issues (e.g., ["ENG", "FEAT"]) */
|
|
6
6
|
teamKeys?: string[];
|