semantic-release-linear-app 0.3.0-next.2 → 0.3.0-next.4

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.
@@ -15,30 +15,30 @@ async function verifyConditions(pluginConfig, context) {
15
15
  // Check for API key in config or environment
16
16
  const linearApiKey = apiKey || process.env.LINEAR_API_KEY;
17
17
  if (!linearApiKey) {
18
- throw new error_1.default("No Linear API key found", "ENOLINEARTOKEN", "Please provide a Linear API key via plugin config or LINEAR_API_KEY environment variable.");
18
+ throw new error_1.default('No Linear API key found', 'ENOLINEARTOKEN', 'Please provide a Linear API key via plugin config or LINEAR_API_KEY environment variable.');
19
19
  }
20
20
  // Validate team keys format if provided
21
21
  const teamKeyPattern = /^[A-Z]+$/;
22
22
  const branchPattern = /^[A-Za-z0-9._-]+\/[A-Za-z0-9][A-Za-z0-9._-]*$/;
23
23
  const invalidTeamKeys = teamKeys.filter((key) => !teamKeyPattern.test(key) && !branchPattern.test(key));
24
24
  if (invalidTeamKeys.length > 0) {
25
- throw new error_1.default("Invalid team key format", "EINVALIDTEAMKEY", "Team keys must be uppercase letters (e.g. SD) or branch names (e.g. caio/tk-519-title). " +
26
- `Invalid: ${invalidTeamKeys.join(", ")}`);
25
+ throw new error_1.default('Invalid team key format', 'EINVALIDTEAMKEY', 'Team keys must be uppercase letters (e.g. SD) or branch names (e.g. caio/tk-519-title). ' +
26
+ `Invalid: ${invalidTeamKeys.join(', ')}`);
27
27
  }
28
28
  // Test API connection
29
29
  const client = new linear_client_1.LinearClient(linearApiKey);
30
30
  try {
31
- logger.log("Verifying Linear API access...");
31
+ logger.log('Verifying Linear API access...');
32
32
  await client.testConnection();
33
- logger.log("✓ Linear API access verified");
33
+ logger.log('✓ Linear API access verified');
34
34
  }
35
35
  catch (error) {
36
- throw new error_1.default("Failed to connect to Linear API", "ELINEARCONNECTION", `Could not connect to Linear API: ${error.message}`);
36
+ throw new error_1.default('Failed to connect to Linear API', 'ELINEARCONNECTION', `Could not connect to Linear API: ${error.message}`);
37
37
  }
38
38
  // Store validated config in context for other lifecycle methods
39
39
  context.linear = {
40
40
  apiKey: linearApiKey,
41
41
  teamKeys: teamKeys.length > 0 ? teamKeys : null,
42
- labelPrefix: pluginConfig.labelPrefix || "v",
42
+ labelPrefix: pluginConfig.labelPrefix || 'v',
43
43
  };
44
44
  }
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  // Minimal ESM mocks
4
- jest.mock("@semantic-release/error", () => {
4
+ jest.mock('@semantic-release/error', () => {
5
5
  class SemanticReleaseError extends Error {
6
6
  code;
7
7
  constructor(message, code) {
@@ -11,32 +11,32 @@ jest.mock("@semantic-release/error", () => {
11
11
  }
12
12
  return { __esModule: true, default: SemanticReleaseError };
13
13
  });
14
- jest.mock("./linear-client", () => ({
14
+ jest.mock('./linear-client', () => ({
15
15
  LinearClient: jest.fn().mockImplementation(() => ({
16
16
  testConnection: jest.fn().mockResolvedValue({}),
17
17
  })),
18
18
  }));
19
- jest.mock("node-fetch", () => ({ __esModule: true, default: jest.fn() }));
19
+ jest.mock('node-fetch', () => ({ __esModule: true, default: jest.fn() }));
20
20
  const verify_1 = require("./verify");
21
- describe("verify", () => {
21
+ describe('verify', () => {
22
22
  const mockContext = {
23
23
  logger: { log: jest.fn(), error: jest.fn() },
24
24
  };
25
25
  beforeEach(() => {
26
26
  delete process.env.LINEAR_API_KEY;
27
27
  });
28
- test("throws without API key", async () => {
29
- await expect((0, verify_1.verifyConditions)({}, mockContext)).rejects.toThrow("No Linear API key found");
28
+ test('throws without API key', async () => {
29
+ await expect((0, verify_1.verifyConditions)({}, mockContext)).rejects.toThrow('No Linear API key found');
30
30
  });
31
- test("validates team key format", async () => {
31
+ test('validates team key format', async () => {
32
32
  // Validation happens BEFORE the API call, so it fails fast
33
- await expect((0, verify_1.verifyConditions)({ apiKey: "test", teamKeys: ["eng-123"] }, mockContext)).rejects.toThrow("Invalid team key format");
33
+ await expect((0, verify_1.verifyConditions)({ apiKey: 'test', teamKeys: ['eng-123'] }, mockContext)).rejects.toThrow('Invalid team key format');
34
34
  });
35
- test("accepts valid branch like team key", async () => {
35
+ test('accepts valid branch like team key', async () => {
36
36
  // Accepts branch patterns without hitting API
37
37
  await expect((0, verify_1.verifyConditions)({
38
- apiKey: "test",
39
- teamKeys: ["caio/tk-519-title"],
38
+ apiKey: 'test',
39
+ teamKeys: ['caio/tk-519-title'],
40
40
  }, mockContext)).resolves.toBeUndefined();
41
41
  });
42
42
  });
package/dist/types.d.ts CHANGED
@@ -11,10 +11,6 @@ export interface PluginConfig {
11
11
  addComment?: boolean;
12
12
  /** Preview changes without updating Linear (default: false) */
13
13
  dryRun?: boolean;
14
- /** Branches to skip unless they contain issue IDs (default: ["main", "master", "develop", "staging", "production"]) */
15
- skipBranches?: string[];
16
- /** Require issues in branch name to process (default: true) */
17
- requireIssueInBranch?: boolean;
18
14
  }
19
15
  export interface LinearContext {
20
16
  apiKey: string;
@@ -34,13 +30,8 @@ export interface LinearLabel {
34
30
  name: string;
35
31
  color?: string;
36
32
  }
37
- export interface IssueUpdateResult {
38
- issueId: string;
39
- status: "updated" | "failed" | "not_found";
40
- error?: string;
41
- }
42
33
  export interface LinearViewer {
43
34
  id: string;
44
35
  name: string;
45
36
  }
46
- export type ReleaseType = "major" | "premajor" | "minor" | "preminor" | "patch" | "prepatch" | "prerelease";
37
+ export type ReleaseType = 'major' | 'premajor' | 'minor' | 'preminor' | 'patch' | 'prepatch' | 'prerelease';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "semantic-release-linear-app",
3
- "version": "0.3.0-next.2",
3
+ "version": "0.3.0-next.4",
4
4
  "description": "Semantic-release plugin to update Linear issues with version labels",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -46,29 +46,28 @@
46
46
  },
47
47
  "dependencies": {
48
48
  "@semantic-release/error": "^4.0.0",
49
- "execa": "^9.6.0",
49
+ "execa": "^9.6.1",
50
50
  "node-fetch": "^3.3.2"
51
51
  },
52
52
  "devDependencies": {
53
- "@semantic-release/changelog": "^6.0.3",
54
53
  "@semantic-release/commit-analyzer": "^13.0.1",
55
54
  "@semantic-release/git": "^10.0.1",
56
- "@semantic-release/github": "^11.0.6",
57
- "@semantic-release/npm": "^12.0.2",
55
+ "@semantic-release/github": "^12.0.2",
56
+ "@semantic-release/npm": "^13.1.3",
58
57
  "@semantic-release/release-notes-generator": "^14.1.0",
59
58
  "@types/jest": "^30.0.0",
60
- "@types/node": "^24.7.0",
59
+ "@types/node": "^25.0.1",
61
60
  "@types/node-fetch": "^2.6.13",
62
61
  "@types/semantic-release": "^21.1.0",
63
- "@typescript-eslint/eslint-plugin": "^8.46.0",
64
- "@typescript-eslint/parser": "^8.46.0",
65
- "eslint": "^9.37.0",
62
+ "@typescript-eslint/eslint-plugin": "^8.49.0",
63
+ "@typescript-eslint/parser": "^8.49.0",
64
+ "eslint": "^9.39.2",
66
65
  "husky": "^9.1.7",
67
66
  "jest": "^30.2.0",
68
- "lint-staged": "^16.2.3",
69
- "prettier": "^3.6.2",
70
- "semantic-release": "^24.2.9",
71
- "ts-jest": "^29.4.4",
67
+ "lint-staged": "^16.2.7",
68
+ "prettier": "^3.7.4",
69
+ "semantic-release": "^25.0.2",
70
+ "ts-jest": "^29.4.6",
72
71
  "typescript": "^5.9.3"
73
72
  },
74
73
  "files": [
@@ -1,5 +1,5 @@
1
- import fetch from "node-fetch";
2
- import { LinearIssue, LinearLabel, LinearViewer } from "../types";
1
+ import fetch from 'node-fetch';
2
+ import { LinearIssue, LinearLabel, LinearViewer } from '../types';
3
3
 
4
4
  interface GraphQLResponse<T> {
5
5
  data?: T;
@@ -11,7 +11,7 @@ interface GraphQLResponse<T> {
11
11
  */
12
12
  export class LinearClient {
13
13
  private apiKey: string;
14
- private apiUrl: string = "https://api.linear.app/graphql";
14
+ private apiUrl: string = 'https://api.linear.app/graphql';
15
15
 
16
16
  constructor(apiKey: string) {
17
17
  this.apiKey = apiKey;
@@ -25,10 +25,10 @@ export class LinearClient {
25
25
  variables: Record<string, unknown> = {},
26
26
  ): Promise<T> {
27
27
  const response = await fetch(this.apiUrl, {
28
- method: "POST",
28
+ method: 'POST',
29
29
  headers: {
30
30
  Authorization: this.apiKey,
31
- "Content-Type": "application/json",
31
+ 'Content-Type': 'application/json',
32
32
  },
33
33
  body: JSON.stringify({ query, variables }),
34
34
  });
@@ -40,7 +40,7 @@ export class LinearClient {
40
40
  }
41
41
 
42
42
  if (!data.data) {
43
- throw new Error("No data returned from Linear API");
43
+ throw new Error('No data returned from Linear API');
44
44
  }
45
45
 
46
46
  return data.data;
@@ -66,10 +66,7 @@ export class LinearClient {
66
66
  /**
67
67
  * Find or create a label
68
68
  */
69
- async ensureLabel(
70
- name: string,
71
- color: string = "#4752C4",
72
- ): Promise<LinearLabel> {
69
+ async ensureLabel(name: string, color: string = '#4752C4'): Promise<LinearLabel> {
73
70
  // First, try to find existing label
74
71
  const searchQuery = `
75
72
  query FindLabel($name: String!) {
@@ -143,10 +140,7 @@ export class LinearClient {
143
140
  /**
144
141
  * Add label to issue
145
142
  */
146
- async addLabelToIssue(
147
- issueId: string,
148
- labelId: string,
149
- ): Promise<LinearIssue> {
143
+ async addLabelToIssue(issueId: string, labelId: string): Promise<LinearIssue> {
150
144
  const mutation = `
151
145
  mutation AddLabel($issueId: String!, $labelId: String!) {
152
146
  issueUpdate(
@@ -178,16 +172,11 @@ export class LinearClient {
178
172
  /**
179
173
  * Remove old version labels from issue
180
174
  */
181
- async removeVersionLabels(
182
- issueId: string,
183
- labelPrefix: string,
184
- ): Promise<LinearIssue | null> {
175
+ async removeVersionLabels(issueId: string, labelPrefix: string): Promise<LinearIssue | null> {
185
176
  const issue = await this.getIssue(issueId);
186
177
  if (!issue) return null;
187
178
 
188
- const versionLabels = issue.labels.nodes.filter((label) =>
189
- label.name.startsWith(labelPrefix),
190
- );
179
+ const versionLabels = issue.labels.nodes.filter((label) => label.name.startsWith(labelPrefix));
191
180
 
192
181
  if (versionLabels.length === 0) return issue;
193
182
 
@@ -1,49 +1,28 @@
1
- import { parseIssuesFromBranch, shouldProcessBranch } from "./parse-issues";
1
+ import { parseIssuesFromBranch } from './parse-issues';
2
2
 
3
- describe("parse-issues", () => {
4
- test("extracts Linear issue IDs from branch name", () => {
5
- const branchName = "feature/ENG-123-add-new-feature";
3
+ describe('parse-issues', () => {
4
+ test('extracts Linear issue IDs from branch name', () => {
5
+ const branchName = 'feature/ENG-123-add-new-feature';
6
6
  const result = parseIssuesFromBranch(branchName);
7
- expect(result).toEqual(["ENG-123"]);
7
+ expect(result).toEqual(['ENG-123']);
8
8
  });
9
9
 
10
- test("extracts multiple issue IDs from branch name", () => {
11
- const branchName = "fix/ENG-123-FEAT-456-bug-fix";
10
+ test('extracts multiple issue IDs from branch name', () => {
11
+ const branchName = 'fix/ENG-123-FEAT-456-bug-fix';
12
12
  const result = parseIssuesFromBranch(branchName);
13
- expect(result).toEqual(expect.arrayContaining(["ENG-123", "FEAT-456"]));
13
+ expect(result).toEqual(expect.arrayContaining(['ENG-123', 'FEAT-456']));
14
14
  expect(result).toHaveLength(2);
15
15
  });
16
16
 
17
- test("filters by team keys when provided", () => {
18
- const branchName = "feature/ENG-123-OTHER-456";
19
- const result = parseIssuesFromBranch(branchName, ["ENG"]);
20
- expect(result).toEqual(["ENG-123"]);
17
+ test('filters by team keys when provided', () => {
18
+ const branchName = 'feature/ENG-123-OTHER-456';
19
+ const result = parseIssuesFromBranch(branchName, ['ENG']);
20
+ expect(result).toEqual(['ENG-123']);
21
21
  });
22
22
 
23
- test("returns empty array for branch without issues", () => {
24
- const branchName = "feature/no-issues-here";
23
+ test('returns empty array for branch without issues', () => {
24
+ const branchName = 'feature/no-issues-here';
25
25
  const result = parseIssuesFromBranch(branchName);
26
26
  expect(result).toEqual([]);
27
27
  });
28
-
29
- test("shouldProcessBranch returns false for skip branches without issues", () => {
30
- expect(shouldProcessBranch("main", ["main", "master"])).toBe(false);
31
- expect(shouldProcessBranch("master", ["main", "master"])).toBe(false);
32
- });
33
-
34
- test("shouldProcessBranch returns true for skip branches with issues", () => {
35
- expect(shouldProcessBranch("main-ENG-123", ["main", "master"])).toBe(true);
36
- });
37
-
38
- test("shouldProcessBranch returns true for feature branches with issues", () => {
39
- expect(shouldProcessBranch("feature/ENG-123", ["main", "master"])).toBe(
40
- true,
41
- );
42
- });
43
-
44
- test("shouldProcessBranch returns false for feature branches without issues", () => {
45
- expect(shouldProcessBranch("feature/no-issues", ["main", "master"])).toBe(
46
- false,
47
- );
48
- });
49
28
  });
@@ -16,10 +16,10 @@ export function parseIssuesFromBranch(
16
16
  const issues = new Set<string>();
17
17
 
18
18
  // Build regex pattern based on team keys
19
- const teamPattern = teamKeys ? `(?:${teamKeys.join("|")})` : "[A-Z]+";
19
+ const teamPattern = teamKeys ? `(?:${teamKeys.join('|')})` : '[A-Z]+';
20
20
 
21
21
  // Pattern matches: feature/ENG-123-description, ENG-123, bugfix/FEAT-45, etc.
22
- const issuePattern = new RegExp(`\\b(${teamPattern}-\\d+)\\b`, "gi");
22
+ const issuePattern = new RegExp(`\\b(${teamPattern}-\\d+)\\b`, 'gi');
23
23
 
24
24
  const matches = Array.from(branchName.matchAll(issuePattern));
25
25
  for (const match of matches) {
@@ -28,30 +28,3 @@ export function parseIssuesFromBranch(
28
28
 
29
29
  return Array.from(issues);
30
30
  }
31
-
32
- /**
33
- * Check if branch should be processed for Linear updates
34
- * @param branchName - The branch name to check
35
- * @param skipBranches - Branches to skip (default: main, master, develop without issue IDs)
36
- * @returns true if branch should be processed
37
- */
38
- export function shouldProcessBranch(
39
- branchName: string,
40
- skipBranches: string[] = [
41
- "main",
42
- "master",
43
- "develop",
44
- "staging",
45
- "production",
46
- ],
47
- ): boolean {
48
- // Skip certain branches UNLESS they contain an issue ID
49
- if (skipBranches.includes(branchName)) {
50
- // These branches are OK if they contain an issue ID
51
- const hasIssue = /[A-Z]+-\d+/.test(branchName);
52
- return hasIssue;
53
- }
54
-
55
- // Process any other branch that contains an issue ID
56
- return /[A-Z]+-\d+/.test(branchName);
57
- }
@@ -1,8 +1,8 @@
1
- import { SuccessContext } from "semantic-release";
2
- import { execa } from "execa";
3
- import { LinearClient } from "./linear-client";
4
- import { parseIssuesFromBranch } from "./parse-issues";
5
- import { PluginConfig, LinearContext, ReleaseType } from "../types";
1
+ import { SuccessContext } from 'semantic-release';
2
+ import { execa } from 'execa';
3
+ import { LinearClient } from './linear-client';
4
+ import { parseIssuesFromBranch } from './parse-issues';
5
+ import { PluginConfig, LinearContext, ReleaseType } from '../types';
6
6
 
7
7
  interface ExtendedContext extends SuccessContext {
8
8
  linear?: LinearContext;
@@ -13,37 +13,38 @@ interface ExtendedContext extends SuccessContext {
13
13
  */
14
14
  async function findSourceBranches(
15
15
  commits: readonly { hash: string }[],
16
- logger: SuccessContext["logger"],
16
+ logger: SuccessContext['logger'],
17
17
  ): Promise<Set<string>> {
18
18
  const branches = new Set<string>();
19
+ const skipBranches = ['main', 'master', 'develop', 'stable', 'HEAD'];
19
20
 
20
21
  if (commits.length === 0) return branches;
21
22
 
22
- try {
23
- // Get all branches that contain these commits
24
- const { stdout } = await execa("git", [
25
- "branch",
26
- "-r",
27
- "--contains",
28
- commits[0].hash,
29
- "--merged",
30
- ]);
31
-
32
- // Parse remote branch names
33
- const branchLines = stdout.split("\n").map((b: string) => b.trim());
34
- for (const line of branchLines) {
35
- // Extract branch name from origin/feature/ENG-123-description
36
- const match = line.match(/origin\/(.+)/);
37
- if (
38
- match &&
39
- !["main", "master", "develop", "stable", "HEAD"].includes(match[1])
40
- ) {
41
- branches.add(match[1]);
42
- logger.log(`Found source branch: ${match[1]}`);
23
+ // Check all commits to find all source branches
24
+ for (const commit of commits) {
25
+ try {
26
+ const { stdout } = await execa('git', [
27
+ 'branch',
28
+ '-r',
29
+ '--contains',
30
+ commit.hash,
31
+ '--merged',
32
+ ]);
33
+
34
+ const branchLines = stdout.split('\n').map((b: string) => b.trim());
35
+ for (const line of branchLines) {
36
+ const match = line.match(/origin\/(.+)/);
37
+ if (match && !skipBranches.includes(match[1])) {
38
+ branches.add(match[1]);
39
+ }
43
40
  }
41
+ } catch {
42
+ // Commit might not exist or other git error, continue with next commit
44
43
  }
45
- } catch (error) {
46
- logger.debug(`Git branch lookup failed: ${(error as Error).message}`);
44
+ }
45
+
46
+ if (branches.size > 0) {
47
+ logger.log(`Found source branches: ${Array.from(branches).join(', ')}`);
47
48
  }
48
49
 
49
50
  return branches;
@@ -52,33 +53,26 @@ async function findSourceBranches(
52
53
  /**
53
54
  * Update Linear issues after a successful release
54
55
  */
55
- export async function success(
56
- pluginConfig: PluginConfig,
57
- context: ExtendedContext,
58
- ): Promise<void> {
56
+ export async function success(pluginConfig: PluginConfig, context: ExtendedContext): Promise<void> {
59
57
  const { logger, nextRelease, linear, commits } = context;
60
58
 
61
59
  if (!linear) {
62
- logger.log("Linear context not found, skipping issue updates");
60
+ logger.log('Linear context not found, skipping issue updates');
63
61
  return;
64
62
  }
65
63
 
66
64
  if (!commits || commits.length === 0) {
67
- logger.log("No commits found in release, skipping Linear updates");
65
+ logger.log('No commits found in release, skipping Linear updates');
68
66
  return;
69
67
  }
70
68
 
71
- const {
72
- removeOldLabels = true,
73
- addComment = false,
74
- dryRun = false,
75
- } = pluginConfig;
69
+ const { removeOldLabels = true, addComment = false, dryRun = false } = pluginConfig;
76
70
 
77
71
  // Find all branches that contributed to this release
78
72
  const sourceBranches = await findSourceBranches(commits, logger);
79
73
 
80
74
  if (sourceBranches.size === 0) {
81
- logger.log("No source branches found, skipping Linear updates");
75
+ logger.log('No source branches found, skipping Linear updates');
82
76
  return;
83
77
  }
84
78
 
@@ -90,30 +84,30 @@ export async function success(
90
84
  }
91
85
 
92
86
  if (issueIds.size === 0) {
93
- logger.log(
94
- `No Linear issues found in branches: ${Array.from(sourceBranches).join(", ")}`,
95
- );
87
+ logger.log(`No Linear issues found in branches: ${Array.from(sourceBranches).join(', ')}`);
96
88
  return;
97
89
  }
98
90
 
99
91
  logger.log(
100
- `Found ${issueIds.size} Linear issue(s): ${Array.from(issueIds).join(", ")} ` +
92
+ `Found ${issueIds.size} Linear issue(s): ${Array.from(issueIds).join(', ')} ` +
101
93
  `from ${sourceBranches.size} branch(es)`,
102
94
  );
103
95
 
96
+ // Build label name with optional channel suffix
97
+ const version = nextRelease.version;
98
+ const channel = nextRelease.channel;
99
+ const labelName = channel
100
+ ? `${linear.labelPrefix}${version}-${channel}`
101
+ : `${linear.labelPrefix}${version}`;
102
+
104
103
  if (dryRun) {
105
- logger.log("[Dry run] Would update issues:", Array.from(issueIds));
106
- logger.log(
107
- `[Dry run] Would apply label: ${linear.labelPrefix}${nextRelease.version}`,
108
- );
104
+ logger.log('[Dry run] Would update issues:', Array.from(issueIds));
105
+ logger.log(`[Dry run] Would apply label: ${labelName}`);
109
106
  return;
110
107
  }
111
108
 
112
109
  // Initialize Linear client and prepare label
113
110
  const client = new LinearClient(linear.apiKey);
114
- const version = nextRelease.version;
115
- const channel = nextRelease.channel || "latest";
116
- const labelName = `${linear.labelPrefix}${version}`;
117
111
  const labelColor = getLabelColor(nextRelease.type as ReleaseType);
118
112
 
119
113
  // Ensure the version label exists
@@ -127,7 +121,7 @@ export async function success(
127
121
  const issue = await client.getIssue(issueId);
128
122
  if (!issue) {
129
123
  logger.warn(`Issue ${issueId} not found in Linear`);
130
- return { issueId, status: "not_found" };
124
+ return { issueId, status: 'not_found' };
131
125
  }
132
126
 
133
127
  // Remove old version labels if configured
@@ -140,37 +134,34 @@ export async function success(
140
134
 
141
135
  // Add comment if configured
142
136
  if (addComment) {
143
- const emoji = channel === "latest" ? "🚀" : "🔬";
144
- const channelText =
145
- channel === "latest" ? "" : ` (${channel} channel)`;
137
+ const emoji = channel ? '🔬' : '🚀';
138
+ const channelText = channel ? ` (${channel} channel)` : '';
146
139
  const comment = `${emoji} Released in version ${version}${channelText}`;
147
140
  await client.addComment(issue.id, comment);
148
141
  }
149
142
 
150
143
  logger.log(`✓ Updated issue ${issueId}`);
151
- return { issueId, status: "updated" };
144
+ return { issueId, status: 'updated' };
152
145
  } catch (error) {
153
146
  const message = error instanceof Error ? error.message : String(error);
154
147
  logger.error(`Failed to update issue ${issueId}: ${message}`);
155
- return { issueId, status: "failed", error: message };
148
+ return { issueId, status: 'failed', error: message };
156
149
  }
157
150
  }),
158
151
  );
159
152
 
160
153
  // Log summary
161
154
  const updated = results.filter(
162
- (r) => r.status === "fulfilled" && r.value?.status === "updated",
155
+ (r) => r.status === 'fulfilled' && r.value?.status === 'updated',
163
156
  ).length;
164
157
  const failed = results.filter(
165
- (r) => r.status === "rejected" || r.value?.status === "failed",
158
+ (r) => r.status === 'rejected' || r.value?.status === 'failed',
166
159
  ).length;
167
160
  const notFound = results.filter(
168
- (r) => r.status === "fulfilled" && r.value?.status === "not_found",
161
+ (r) => r.status === 'fulfilled' && r.value?.status === 'not_found',
169
162
  ).length;
170
163
 
171
- logger.log(
172
- `Linear update complete: ${updated} updated, ${failed} failed, ${notFound} not found`,
173
- );
164
+ logger.log(`Linear update complete: ${updated} updated, ${failed} failed, ${notFound} not found`);
174
165
  }
175
166
 
176
167
  /**
@@ -178,14 +169,14 @@ export async function success(
178
169
  */
179
170
  function getLabelColor(releaseType: ReleaseType): string {
180
171
  const colors: Record<ReleaseType, string> = {
181
- major: "#F44336", // Red for breaking changes
182
- premajor: "#E91E63", // Pink for pre-major
183
- minor: "#FF9800", // Orange for new features
184
- preminor: "#FFC107", // Amber for pre-minor
185
- patch: "#4CAF50", // Green for fixes
186
- prepatch: "#8BC34A", // Light green for pre-patch
187
- prerelease: "#9C27B0", // Purple for prereleases
172
+ major: '#F44336', // Red for breaking changes
173
+ premajor: '#E91E63', // Pink for pre-major
174
+ minor: '#FF9800', // Orange for new features
175
+ preminor: '#FFC107', // Amber for pre-minor
176
+ patch: '#4CAF50', // Green for fixes
177
+ prepatch: '#8BC34A', // Light green for pre-patch
178
+ prerelease: '#9C27B0', // Purple for prereleases
188
179
  };
189
180
 
190
- return colors[releaseType] || "#4752C4"; // Default blue
181
+ return colors[releaseType] || '#4752C4'; // Default blue
191
182
  }
@@ -1,5 +1,5 @@
1
1
  // Minimal ESM mocks
2
- jest.mock("@semantic-release/error", () => {
2
+ jest.mock('@semantic-release/error', () => {
3
3
  class SemanticReleaseError extends Error {
4
4
  code: string;
5
5
  constructor(message: string, code: string) {
@@ -10,18 +10,18 @@ jest.mock("@semantic-release/error", () => {
10
10
  return { __esModule: true, default: SemanticReleaseError };
11
11
  });
12
12
 
13
- jest.mock("./linear-client", () => ({
13
+ jest.mock('./linear-client', () => ({
14
14
  LinearClient: jest.fn().mockImplementation(() => ({
15
15
  testConnection: jest.fn().mockResolvedValue({}),
16
16
  })),
17
17
  }));
18
18
 
19
- jest.mock("node-fetch", () => ({ __esModule: true, default: jest.fn() }));
19
+ jest.mock('node-fetch', () => ({ __esModule: true, default: jest.fn() }));
20
20
 
21
- import { verifyConditions } from "./verify";
22
- import { VerifyConditionsContext } from "semantic-release";
21
+ import { verifyConditions } from './verify';
22
+ import { VerifyConditionsContext } from 'semantic-release';
23
23
 
24
- describe("verify", () => {
24
+ describe('verify', () => {
25
25
  const mockContext = {
26
26
  logger: { log: jest.fn(), error: jest.fn() },
27
27
  } as VerifyConditionsContext;
@@ -30,26 +30,24 @@ describe("verify", () => {
30
30
  delete process.env.LINEAR_API_KEY;
31
31
  });
32
32
 
33
- test("throws without API key", async () => {
34
- await expect(verifyConditions({}, mockContext)).rejects.toThrow(
35
- "No Linear API key found",
36
- );
33
+ test('throws without API key', async () => {
34
+ await expect(verifyConditions({}, mockContext)).rejects.toThrow('No Linear API key found');
37
35
  });
38
36
 
39
- test("validates team key format", async () => {
37
+ test('validates team key format', async () => {
40
38
  // Validation happens BEFORE the API call, so it fails fast
41
39
  await expect(
42
- verifyConditions({ apiKey: "test", teamKeys: ["eng-123"] }, mockContext),
43
- ).rejects.toThrow("Invalid team key format");
40
+ verifyConditions({ apiKey: 'test', teamKeys: ['eng-123'] }, mockContext),
41
+ ).rejects.toThrow('Invalid team key format');
44
42
  });
45
43
 
46
- test("accepts valid branch like team key", async () => {
44
+ test('accepts valid branch like team key', async () => {
47
45
  // Accepts branch patterns without hitting API
48
46
  await expect(
49
47
  verifyConditions(
50
48
  {
51
- apiKey: "test",
52
- teamKeys: ["caio/tk-519-title"],
49
+ apiKey: 'test',
50
+ teamKeys: ['caio/tk-519-title'],
53
51
  },
54
52
  mockContext,
55
53
  ),