semantic-release-linear-app 0.3.0-next.2 โ†’ 0.3.0-next.3

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 CHANGED
@@ -1,21 +1,20 @@
1
- # semantic-release-linear
1
+ # semantic-release-linear-app
2
2
 
3
3
  > A [semantic-release](https://github.com/semantic-release/semantic-release) plugin that updates Linear issues with version labels based on branch names.
4
4
 
5
5
  ## Features
6
6
 
7
- - ๐ŸŒฟ Extracts Linear issue IDs from branch names
8
- - ๐Ÿท๏ธ Automatically adds version labels to Linear issues
9
- - ๐ŸŽจ Color-coded labels based on release type
10
- - ๐Ÿงน Removes old version labels (configurable)
11
- - ๐Ÿ’ฌ Adds release comments to issues (optional)
12
- - โšก Batch operations for efficiency
13
- - ๐Ÿ“ Full TypeScript support
7
+ - Extracts Linear issue IDs from branch names
8
+ - Adds version labels to Linear issues (e.g., `v1.2.3` or `v1.2.3-beta`)
9
+ - Color-coded labels based on release type
10
+ - Supports multiple release channels (beta, next, stable, etc.)
11
+ - Removes old version labels (configurable)
12
+ - Adds release comments to issues (optional)
14
13
 
15
14
  ## Install
16
15
 
17
16
  ```bash
18
- npm install --save-dev semantic-release-linear
17
+ npm install --save-dev semantic-release-linear-app
19
18
  ```
20
19
 
21
20
  ## Quick Start
@@ -27,7 +26,7 @@ npm install --save-dev semantic-release-linear
27
26
  "plugins": [
28
27
  "@semantic-release/commit-analyzer",
29
28
  "@semantic-release/release-notes-generator",
30
- ["semantic-release-linear", {
29
+ ["semantic-release-linear-app", {
31
30
  "teamKeys": ["ENG", "FEAT", "BUG"]
32
31
  }],
33
32
  "@semantic-release/github"
@@ -62,28 +61,25 @@ That's it! When semantic-release creates a release from these branches, the corr
62
61
  | `labelPrefix` | `"v"` | Prefix for version labels |
63
62
  | `removeOldLabels` | `true` | Remove previous version labels |
64
63
  | `addComment` | `false` | Add a release comment to issues |
65
- | `skipBranches` | `["main", "master", "develop", "staging", "production"]` | Branches to skip unless they contain issues |
66
- | `requireIssueInBranch` | `true` | Only process branches with Linear issues |
67
64
  | `dryRun` | `false` | Preview without making changes |
68
65
 
69
66
  ### TypeScript Configuration
70
67
 
71
68
  ```typescript
72
69
  // .releaserc.ts
73
- import type { PluginConfig } from 'semantic-release-linear';
70
+ import type { PluginConfig } from 'semantic-release-linear-app';
74
71
 
75
72
  const config: PluginConfig = {
76
73
  teamKeys: ['ENG', 'FEAT'],
77
- labelPrefix: 'release-',
78
- addComment: true,
79
- skipBranches: ['main', 'develop']
74
+ labelPrefix: 'v',
75
+ addComment: true
80
76
  };
81
77
 
82
78
  export default {
83
79
  plugins: [
84
80
  '@semantic-release/commit-analyzer',
85
81
  '@semantic-release/release-notes-generator',
86
- ['semantic-release-linear', config],
82
+ ['semantic-release-linear-app', config],
87
83
  '@semantic-release/github'
88
84
  ]
89
85
  };
@@ -138,29 +134,41 @@ fi
138
134
 
139
135
  ## How It Works
140
136
 
141
- 1. **Branch Detection**: When semantic-release runs, it reads the current branch name
142
- 2. **Issue Extraction**: Extracts Linear issue IDs (e.g., `ENG-123`) from the branch
143
- 3. **Label Creation**: Creates a version label if needed (e.g., `v1.2.3`)
144
- 4. **Issue Update**: Applies the label to the Linear issue(s)
145
- 5. **Cleanup**: Optionally removes old version labels
137
+ 1. **Commit Analysis**: When semantic-release creates a release, the plugin scans all commits
138
+ 2. **Branch Detection**: Finds all source branches that contributed commits to this release
139
+ 3. **Issue Extraction**: Extracts Linear issue IDs (e.g., `ENG-123`) from branch names
140
+ 4. **Label Creation**: Creates a version label (e.g., `v1.2.3` or `v1.2.3-beta`)
141
+ 5. **Issue Update**: Applies the label to the Linear issue(s)
142
+ 6. **Cleanup**: Optionally removes old version labels
143
+
144
+ ### Channel Labels
145
+
146
+ When releasing to different channels, labels include the channel suffix:
147
+
148
+ | Channel | Label Example |
149
+ |---------|---------------|
150
+ | Default (latest) | `v1.2.3` |
151
+ | beta | `v1.2.3-beta` |
152
+ | next | `v1.2.3-next` |
153
+ | alpha | `v1.2.3-alpha` |
146
154
 
147
155
  ### Label Colors
148
156
 
149
157
  Labels are color-coded by release type:
150
158
 
151
- - ๐Ÿ”ด **Major** (breaking changes) - Red
152
- - ๐ŸŸ  **Minor** (new features) - Orange
153
- - ๐ŸŸข **Patch** (bug fixes) - Green
154
- - ๐ŸŸฃ **Prerelease** - Purple
159
+ - **Major** (breaking changes) - Red
160
+ - **Minor** (new features) - Orange
161
+ - **Patch** (bug fixes) - Green
162
+ - **Prerelease** - Purple
155
163
 
156
164
  ## Dry Run Mode
157
165
 
158
166
  Test what will happen without making changes:
159
167
 
160
- ```javascript
168
+ ```json
161
169
  {
162
170
  "plugins": [
163
- ["semantic-release-linear", {
171
+ ["semantic-release-linear-app", {
164
172
  "dryRun": true
165
173
  }]
166
174
  ]
@@ -169,9 +177,9 @@ Test what will happen without making changes:
169
177
 
170
178
  Output:
171
179
  ```
172
- [semantic-release] [semantic-release-linear] Found 1 Linear issue(s) in branch "feature/ENG-123-auth": ENG-123
173
- [semantic-release] [semantic-release-linear] [Dry run] Would update issues: ["ENG-123"]
174
- [semantic-release] [semantic-release-linear] [Dry run] Would apply label: v1.2.3
180
+ [semantic-release-linear-app] Found 1 Linear issue(s): ENG-123 from 1 branch(es)
181
+ [semantic-release-linear-app] [Dry run] Would update issues: ["ENG-123"]
182
+ [semantic-release-linear-app] [Dry run] Would apply label: v1.2.3
175
183
  ```
176
184
 
177
185
  ## Linear API Setup
@@ -223,14 +231,6 @@ Extracting from commits is unreliable - developers forget, commits get squashed,
223
231
  - Visible in PR lists
224
232
  - A single source of truth
225
233
 
226
- ### What about releases from main/master?
227
-
228
- You have three options:
229
-
230
- 1. **Skip them** (default): Set `requireIssueInBranch: true`
231
- 2. **Tag main with an issue**: `git checkout main-ENG-123`
232
- 3. **Allow all branches**: Set `requireIssueInBranch: false`
233
-
234
234
  ### Can I update multiple issues?
235
235
 
236
236
  Yes, include multiple issue IDs in your branch name:
@@ -240,7 +240,7 @@ feature/ENG-123-FEAT-456-big-feature
240
240
 
241
241
  ### What if my branch doesn't have an issue?
242
242
 
243
- The plugin will skip it (unless `requireIssueInBranch: false`). This is intentional - not every release needs to update Linear.
243
+ The plugin will skip branches without Linear issue IDs. This is intentional - not every release needs to update Linear.
244
244
 
245
245
  ## Troubleshooting
246
246
 
@@ -9,10 +9,3 @@
9
9
  * @returns Array of unique issue identifiers
10
10
  */
11
11
  export declare function parseIssuesFromBranch(branchName: string, teamKeys?: string[] | null): string[];
12
- /**
13
- * Check if branch should be processed for Linear updates
14
- * @param branchName - The branch name to check
15
- * @param skipBranches - Branches to skip (default: main, master, develop without issue IDs)
16
- * @returns true if branch should be processed
17
- */
18
- export declare function shouldProcessBranch(branchName: string, skipBranches?: string[]): boolean;
@@ -5,7 +5,6 @@
5
5
  */
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
7
  exports.parseIssuesFromBranch = parseIssuesFromBranch;
8
- exports.shouldProcessBranch = shouldProcessBranch;
9
8
  /**
10
9
  * Extract Linear issue IDs from a branch name
11
10
  * @param branchName - The branch name to parse
@@ -15,34 +14,12 @@ exports.shouldProcessBranch = shouldProcessBranch;
15
14
  function parseIssuesFromBranch(branchName, teamKeys = null) {
16
15
  const issues = new Set();
17
16
  // Build regex pattern based on team keys
18
- const teamPattern = teamKeys ? `(?:${teamKeys.join("|")})` : "[A-Z]+";
17
+ const teamPattern = teamKeys ? `(?:${teamKeys.join('|')})` : '[A-Z]+';
19
18
  // Pattern matches: feature/ENG-123-description, ENG-123, bugfix/FEAT-45, etc.
20
- const issuePattern = new RegExp(`\\b(${teamPattern}-\\d+)\\b`, "gi");
19
+ const issuePattern = new RegExp(`\\b(${teamPattern}-\\d+)\\b`, 'gi');
21
20
  const matches = Array.from(branchName.matchAll(issuePattern));
22
21
  for (const match of matches) {
23
22
  issues.add(match[1].toUpperCase());
24
23
  }
25
24
  return Array.from(issues);
26
25
  }
27
- /**
28
- * Check if branch should be processed for Linear updates
29
- * @param branchName - The branch name to check
30
- * @param skipBranches - Branches to skip (default: main, master, develop without issue IDs)
31
- * @returns true if branch should be processed
32
- */
33
- function shouldProcessBranch(branchName, skipBranches = [
34
- "main",
35
- "master",
36
- "develop",
37
- "staging",
38
- "production",
39
- ]) {
40
- // Skip certain branches UNLESS they contain an issue ID
41
- if (skipBranches.includes(branchName)) {
42
- // These branches are OK if they contain an issue ID
43
- const hasIssue = /[A-Z]+-\d+/.test(branchName);
44
- return hasIssue;
45
- }
46
- // Process any other branch that contains an issue ID
47
- return /[A-Z]+-\d+/.test(branchName);
48
- }
@@ -1,39 +1,26 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const parse_issues_1 = require("./parse-issues");
4
- describe("parse-issues", () => {
5
- test("extracts Linear issue IDs from branch name", () => {
6
- const branchName = "feature/ENG-123-add-new-feature";
4
+ describe('parse-issues', () => {
5
+ test('extracts Linear issue IDs from branch name', () => {
6
+ const branchName = 'feature/ENG-123-add-new-feature';
7
7
  const result = (0, parse_issues_1.parseIssuesFromBranch)(branchName);
8
- expect(result).toEqual(["ENG-123"]);
8
+ expect(result).toEqual(['ENG-123']);
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 = (0, parse_issues_1.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
- test("filters by team keys when provided", () => {
17
- const branchName = "feature/ENG-123-OTHER-456";
18
- const result = (0, parse_issues_1.parseIssuesFromBranch)(branchName, ["ENG"]);
19
- expect(result).toEqual(["ENG-123"]);
16
+ test('filters by team keys when provided', () => {
17
+ const branchName = 'feature/ENG-123-OTHER-456';
18
+ const result = (0, parse_issues_1.parseIssuesFromBranch)(branchName, ['ENG']);
19
+ expect(result).toEqual(['ENG-123']);
20
20
  });
21
- test("returns empty array for branch without issues", () => {
22
- const branchName = "feature/no-issues-here";
21
+ test('returns empty array for branch without issues', () => {
22
+ const branchName = 'feature/no-issues-here';
23
23
  const result = (0, parse_issues_1.parseIssuesFromBranch)(branchName);
24
24
  expect(result).toEqual([]);
25
25
  });
26
- test("shouldProcessBranch returns false for skip branches without issues", () => {
27
- expect((0, parse_issues_1.shouldProcessBranch)("main", ["main", "master"])).toBe(false);
28
- expect((0, parse_issues_1.shouldProcessBranch)("master", ["main", "master"])).toBe(false);
29
- });
30
- test("shouldProcessBranch returns true for skip branches with issues", () => {
31
- expect((0, parse_issues_1.shouldProcessBranch)("main-ENG-123", ["main", "master"])).toBe(true);
32
- });
33
- test("shouldProcessBranch returns true for feature branches with issues", () => {
34
- expect((0, parse_issues_1.shouldProcessBranch)("feature/ENG-123", ["main", "master"])).toBe(true);
35
- });
36
- test("shouldProcessBranch returns false for feature branches without issues", () => {
37
- expect((0, parse_issues_1.shouldProcessBranch)("feature/no-issues", ["main", "master"])).toBe(false);
38
- });
39
26
  });
@@ -1,5 +1,5 @@
1
- import { SuccessContext } from "semantic-release";
2
- import { PluginConfig, LinearContext } from "../types";
1
+ import { SuccessContext } from 'semantic-release';
2
+ import { PluginConfig, LinearContext } from '../types';
3
3
  interface ExtendedContext extends SuccessContext {
4
4
  linear?: LinearContext;
5
5
  }
@@ -9,31 +9,33 @@ const parse_issues_1 = require("./parse-issues");
9
9
  */
10
10
  async function findSourceBranches(commits, logger) {
11
11
  const branches = new Set();
12
+ const skipBranches = ['main', 'master', 'develop', 'stable', 'HEAD'];
12
13
  if (commits.length === 0)
13
14
  return branches;
14
- try {
15
- // Get all branches that contain these commits
16
- const { stdout } = await (0, execa_1.execa)("git", [
17
- "branch",
18
- "-r",
19
- "--contains",
20
- commits[0].hash,
21
- "--merged",
22
- ]);
23
- // Parse remote branch names
24
- const branchLines = stdout.split("\n").map((b) => b.trim());
25
- for (const line of branchLines) {
26
- // Extract branch name from origin/feature/ENG-123-description
27
- const match = line.match(/origin\/(.+)/);
28
- if (match &&
29
- !["main", "master", "develop", "stable", "HEAD"].includes(match[1])) {
30
- branches.add(match[1]);
31
- logger.log(`Found source branch: ${match[1]}`);
15
+ // Check all commits to find all source branches
16
+ for (const commit of commits) {
17
+ try {
18
+ const { stdout } = await (0, execa_1.execa)('git', [
19
+ 'branch',
20
+ '-r',
21
+ '--contains',
22
+ commit.hash,
23
+ '--merged',
24
+ ]);
25
+ const branchLines = stdout.split('\n').map((b) => b.trim());
26
+ for (const line of branchLines) {
27
+ const match = line.match(/origin\/(.+)/);
28
+ if (match && !skipBranches.includes(match[1])) {
29
+ branches.add(match[1]);
30
+ }
32
31
  }
33
32
  }
33
+ catch {
34
+ // Commit might not exist or other git error, continue with next commit
35
+ }
34
36
  }
35
- catch (error) {
36
- logger.debug(`Git branch lookup failed: ${error.message}`);
37
+ if (branches.size > 0) {
38
+ logger.log(`Found source branches: ${Array.from(branches).join(', ')}`);
37
39
  }
38
40
  return branches;
39
41
  }
@@ -43,18 +45,18 @@ async function findSourceBranches(commits, logger) {
43
45
  async function success(pluginConfig, context) {
44
46
  const { logger, nextRelease, linear, commits } = context;
45
47
  if (!linear) {
46
- logger.log("Linear context not found, skipping issue updates");
48
+ logger.log('Linear context not found, skipping issue updates');
47
49
  return;
48
50
  }
49
51
  if (!commits || commits.length === 0) {
50
- logger.log("No commits found in release, skipping Linear updates");
52
+ logger.log('No commits found in release, skipping Linear updates');
51
53
  return;
52
54
  }
53
- const { removeOldLabels = true, addComment = false, dryRun = false, } = pluginConfig;
55
+ const { removeOldLabels = true, addComment = false, dryRun = false } = pluginConfig;
54
56
  // Find all branches that contributed to this release
55
57
  const sourceBranches = await findSourceBranches(commits, logger);
56
58
  if (sourceBranches.size === 0) {
57
- logger.log("No source branches found, skipping Linear updates");
59
+ logger.log('No source branches found, skipping Linear updates');
58
60
  return;
59
61
  }
60
62
  // Extract Linear issue IDs from all found branches
@@ -64,21 +66,24 @@ async function success(pluginConfig, context) {
64
66
  branchIssues.forEach((id) => issueIds.add(id));
65
67
  }
66
68
  if (issueIds.size === 0) {
67
- logger.log(`No Linear issues found in branches: ${Array.from(sourceBranches).join(", ")}`);
69
+ logger.log(`No Linear issues found in branches: ${Array.from(sourceBranches).join(', ')}`);
68
70
  return;
69
71
  }
70
- logger.log(`Found ${issueIds.size} Linear issue(s): ${Array.from(issueIds).join(", ")} ` +
72
+ logger.log(`Found ${issueIds.size} Linear issue(s): ${Array.from(issueIds).join(', ')} ` +
71
73
  `from ${sourceBranches.size} branch(es)`);
74
+ // Build label name with optional channel suffix
75
+ const version = nextRelease.version;
76
+ const channel = nextRelease.channel;
77
+ const labelName = channel
78
+ ? `${linear.labelPrefix}${version}-${channel}`
79
+ : `${linear.labelPrefix}${version}`;
72
80
  if (dryRun) {
73
- logger.log("[Dry run] Would update issues:", Array.from(issueIds));
74
- logger.log(`[Dry run] Would apply label: ${linear.labelPrefix}${nextRelease.version}`);
81
+ logger.log('[Dry run] Would update issues:', Array.from(issueIds));
82
+ logger.log(`[Dry run] Would apply label: ${labelName}`);
75
83
  return;
76
84
  }
77
85
  // Initialize Linear client and prepare label
78
86
  const client = new linear_client_1.LinearClient(linear.apiKey);
79
- const version = nextRelease.version;
80
- const channel = nextRelease.channel || "latest";
81
- const labelName = `${linear.labelPrefix}${version}`;
82
87
  const labelColor = getLabelColor(nextRelease.type);
83
88
  // Ensure the version label exists
84
89
  const label = await client.ensureLabel(labelName, labelColor);
@@ -89,7 +94,7 @@ async function success(pluginConfig, context) {
89
94
  const issue = await client.getIssue(issueId);
90
95
  if (!issue) {
91
96
  logger.warn(`Issue ${issueId} not found in Linear`);
92
- return { issueId, status: "not_found" };
97
+ return { issueId, status: 'not_found' };
93
98
  }
94
99
  // Remove old version labels if configured
95
100
  if (removeOldLabels) {
@@ -99,24 +104,24 @@ async function success(pluginConfig, context) {
99
104
  await client.addLabelToIssue(issue.id, label.id);
100
105
  // Add comment if configured
101
106
  if (addComment) {
102
- const emoji = channel === "latest" ? "๐Ÿš€" : "๐Ÿ”ฌ";
103
- const channelText = channel === "latest" ? "" : ` (${channel} channel)`;
107
+ const emoji = channel ? '๐Ÿ”ฌ' : '๐Ÿš€';
108
+ const channelText = channel ? ` (${channel} channel)` : '';
104
109
  const comment = `${emoji} Released in version ${version}${channelText}`;
105
110
  await client.addComment(issue.id, comment);
106
111
  }
107
112
  logger.log(`โœ“ Updated issue ${issueId}`);
108
- return { issueId, status: "updated" };
113
+ return { issueId, status: 'updated' };
109
114
  }
110
115
  catch (error) {
111
116
  const message = error instanceof Error ? error.message : String(error);
112
117
  logger.error(`Failed to update issue ${issueId}: ${message}`);
113
- return { issueId, status: "failed", error: message };
118
+ return { issueId, status: 'failed', error: message };
114
119
  }
115
120
  }));
116
121
  // Log summary
117
- const updated = results.filter((r) => r.status === "fulfilled" && r.value?.status === "updated").length;
118
- const failed = results.filter((r) => r.status === "rejected" || r.value?.status === "failed").length;
119
- const notFound = results.filter((r) => r.status === "fulfilled" && r.value?.status === "not_found").length;
122
+ const updated = results.filter((r) => r.status === 'fulfilled' && r.value?.status === 'updated').length;
123
+ const failed = results.filter((r) => r.status === 'rejected' || r.value?.status === 'failed').length;
124
+ const notFound = results.filter((r) => r.status === 'fulfilled' && r.value?.status === 'not_found').length;
120
125
  logger.log(`Linear update complete: ${updated} updated, ${failed} failed, ${notFound} not found`);
121
126
  }
122
127
  /**
@@ -124,13 +129,13 @@ async function success(pluginConfig, context) {
124
129
  */
125
130
  function getLabelColor(releaseType) {
126
131
  const colors = {
127
- major: "#F44336", // Red for breaking changes
128
- premajor: "#E91E63", // Pink for pre-major
129
- minor: "#FF9800", // Orange for new features
130
- preminor: "#FFC107", // Amber for pre-minor
131
- patch: "#4CAF50", // Green for fixes
132
- prepatch: "#8BC34A", // Light green for pre-patch
133
- prerelease: "#9C27B0", // Purple for prereleases
132
+ major: '#F44336', // Red for breaking changes
133
+ premajor: '#E91E63', // Pink for pre-major
134
+ minor: '#FF9800', // Orange for new features
135
+ preminor: '#FFC107', // Amber for pre-minor
136
+ patch: '#4CAF50', // Green for fixes
137
+ prepatch: '#8BC34A', // Light green for pre-patch
138
+ prerelease: '#9C27B0', // Purple for prereleases
134
139
  };
135
- return colors[releaseType] || "#4752C4"; // Default blue
140
+ return colors[releaseType] || '#4752C4'; // Default blue
136
141
  }
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.3",
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,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
  }
package/src/types.ts CHANGED
@@ -16,12 +16,6 @@ export interface PluginConfig {
16
16
 
17
17
  /** Preview changes without updating Linear (default: false) */
18
18
  dryRun?: boolean;
19
-
20
- /** Branches to skip unless they contain issue IDs (default: ["main", "master", "develop", "staging", "production"]) */
21
- skipBranches?: string[];
22
-
23
- /** Require issues in branch name to process (default: true) */
24
- requireIssueInBranch?: boolean;
25
19
  }
26
20
 
27
21
  export interface LinearContext {
@@ -45,22 +39,16 @@ export interface LinearLabel {
45
39
  color?: string;
46
40
  }
47
41
 
48
- export interface IssueUpdateResult {
49
- issueId: string;
50
- status: "updated" | "failed" | "not_found";
51
- error?: string;
52
- }
53
-
54
42
  export interface LinearViewer {
55
43
  id: string;
56
44
  name: string;
57
45
  }
58
46
 
59
47
  export type ReleaseType =
60
- | "major"
61
- | "premajor"
62
- | "minor"
63
- | "preminor"
64
- | "patch"
65
- | "prepatch"
66
- | "prerelease";
48
+ | 'major'
49
+ | 'premajor'
50
+ | 'minor'
51
+ | 'preminor'
52
+ | 'patch'
53
+ | 'prepatch'
54
+ | 'prerelease';