semantic-release-linear-app 0.1.0 โ†’ 0.2.0

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,204 +1,261 @@
1
- # semantic-release-linear-app
1
+ # semantic-release-linear
2
2
 
3
- > A [semantic-release](https://github.com/semantic-release/semantic-release) plugin to automatically update Linear issues with version labels when they're included in releases.
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
- - ๐Ÿท๏ธ Automatically adds version labels to Linear issues mentioned in commits
8
- - ๐ŸŽจ Color-coded labels based on release type (major/minor/patch)
9
- - ๐Ÿงน Optionally removes old version labels to keep issues clean
10
- - ๐Ÿ’ฌ Can add release comments to issues (optional)
11
- - ๐Ÿ” Configurable team key filtering
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
12
  - โšก Batch operations for efficiency
13
- - ๐Ÿ“ Full TypeScript support with type definitions
13
+ - ๐Ÿ“ Full TypeScript support
14
14
 
15
15
  ## Install
16
16
 
17
17
  ```bash
18
- npm install --save-dev semantic-release-linear-app
18
+ npm install --save-dev semantic-release-linear
19
19
  ```
20
20
 
21
- ## Usage
21
+ ## Quick Start
22
22
 
23
- Add the plugin to your semantic-release configuration:
23
+ 1. **Configure semantic-release** (`.releaserc.json`):
24
24
 
25
25
  ```json
26
26
  {
27
27
  "plugins": [
28
28
  "@semantic-release/commit-analyzer",
29
29
  "@semantic-release/release-notes-generator",
30
- ["semantic-release-linear-app", {
31
- "apiKey": "lin_api_xxx"
30
+ ["semantic-release-linear", {
31
+ "teamKeys": ["ENG", "FEAT", "BUG"]
32
32
  }],
33
33
  "@semantic-release/github"
34
34
  ]
35
35
  }
36
36
  ```
37
37
 
38
- ### TypeScript Configuration
39
-
40
- If you're using TypeScript for your configuration, the plugin exports types:
41
-
42
- ```typescript
43
- import type { PluginConfig } from 'semantic-release-linear-app';
38
+ 2. **Set your Linear API key**:
44
39
 
45
- const config: PluginConfig = {
46
- apiKey: process.env.LINEAR_API_KEY,
47
- teamKeys: ['ENG', 'FEAT'],
48
- labelPrefix: 'v',
49
- removeOldLabels: true,
50
- addComment: false
51
- };
40
+ ```bash
41
+ export LINEAR_API_KEY=lin_api_xxx
52
42
  ```
53
43
 
54
- ## Configuration
55
-
56
- ### Authentication
57
-
58
- Set your Linear API key via environment variable:
44
+ 3. **Use proper branch names**:
59
45
 
60
46
  ```bash
61
- export LINEAR_API_KEY=lin_api_xxx
47
+ git checkout -b feature/ENG-123-add-authentication
48
+ git checkout -b bugfix/FEAT-456-fix-validation
49
+ git checkout -b ENG-789-quick-fix
62
50
  ```
63
51
 
64
- Or pass it in the plugin configuration:
52
+ That's it! When semantic-release creates a release from these branches, the corresponding Linear issues will be labeled with the version number.
65
53
 
66
- ```json
67
- {
68
- "apiKey": "lin_api_xxx"
69
- }
70
- ```
54
+ ## Configuration
71
55
 
72
56
  ### Options
73
57
 
74
58
  | Option | Default | Description |
75
59
  |--------|---------|-------------|
76
60
  | `apiKey` | - | Linear API key (or use `LINEAR_API_KEY` env var) |
77
- | `teamKeys` | `[]` | Array of team keys to filter issues (e.g., `["ENG", "FEAT"]`) |
61
+ | `teamKeys` | `[]` | Team keys to filter (e.g., `["ENG", "FEAT"]`) |
78
62
  | `labelPrefix` | `"v"` | Prefix for version labels |
79
- | `removeOldLabels` | `true` | Remove previous version labels from issues |
80
- | `addComment` | `false` | Add a comment to issues with release info |
81
- | `dryRun` | `false` | Preview changes without updating Linear |
63
+ | `removeOldLabels` | `true` | Remove previous version labels |
64
+ | `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
+ | `dryRun` | `false` | Preview without making changes |
82
68
 
83
- ### Example Configuration
69
+ ### TypeScript Configuration
84
70
 
85
- ```javascript
86
- // .releaserc.js
87
- module.exports = {
88
- branches: ['main', 'next'],
71
+ ```typescript
72
+ // .releaserc.ts
73
+ import type { PluginConfig } from 'semantic-release-linear';
74
+
75
+ const config: PluginConfig = {
76
+ teamKeys: ['ENG', 'FEAT'],
77
+ labelPrefix: 'release-',
78
+ addComment: true,
79
+ skipBranches: ['main', 'develop']
80
+ };
81
+
82
+ export default {
89
83
  plugins: [
90
84
  '@semantic-release/commit-analyzer',
91
85
  '@semantic-release/release-notes-generator',
92
- ['semantic-release-linear-app', {
93
- teamKeys: ['ENG', 'FEAT', 'BUG'],
94
- labelPrefix: 'version:',
95
- removeOldLabels: true,
96
- addComment: true
97
- }],
98
- '@semantic-release/npm',
86
+ ['semantic-release-linear', config],
99
87
  '@semantic-release/github'
100
88
  ]
101
89
  };
102
90
  ```
103
91
 
104
- ## How It Works
92
+ ## Branch Naming Convention
105
93
 
106
- 1. **Issue Detection**: The plugin scans commit messages for Linear issue IDs (e.g., `ENG-123`)
107
- 2. **Label Creation**: Creates a version label if it doesn't exist (color-coded by release type)
108
- 3. **Issue Updates**: Applies the label to all detected issues
109
- 4. **Cleanup**: Optionally removes old version labels to avoid clutter
110
-
111
- ### Commit Message Examples
112
-
113
- The plugin will detect Linear issues in various formats:
94
+ ### Recommended Patterns
114
95
 
115
96
  ```bash
116
- # In commit message
117
- git commit -m "feat: add new feature ENG-123"
97
+ # Feature branches
98
+ feature/ENG-123-user-authentication
99
+ feature/FEAT-456-payment-integration
118
100
 
119
- # In commit body
120
- git commit -m "feat: add new feature" -m "Closes ENG-123"
101
+ # Bug fixes
102
+ bugfix/BUG-789-login-error
103
+ fix/ENG-101-memory-leak
121
104
 
122
- # Multiple issues
123
- git commit -m "fix: resolve bugs ENG-123, FEAT-456"
105
+ # Hotfixes
106
+ hotfix/ENG-102-critical-security-patch
124
107
 
125
- # In PR title (when squash merging)
126
- "feat: add feature (#123) ENG-456"
108
+ # Simple format (still works)
109
+ ENG-103-quick-update
110
+ FEAT-104
127
111
  ```
128
112
 
129
- ### Label Colors
113
+ ### Multiple Issues in One Branch
130
114
 
131
- Labels are automatically color-coded based on the release type:
115
+ If you need to update multiple issues, include them all in the branch name:
132
116
 
133
- - ๐Ÿ”ด **Major** releases (breaking changes) - Red
134
- - ๐ŸŸ  **Minor** releases (new features) - Orange
135
- - ๐ŸŸข **Patch** releases (bug fixes) - Green
136
- - ๐ŸŸฃ **Prerelease** versions - Purple
117
+ ```bash
118
+ feature/ENG-123-FEAT-456-combined-update
119
+ ```
137
120
 
138
- ## Advanced Usage
121
+ ### Enforce Branch Naming
139
122
 
140
- ### Channel-Specific Configuration
123
+ Use Git hooks or CI checks to enforce the naming convention:
141
124
 
142
- For different behavior on different release channels:
125
+ ```bash
126
+ #!/bin/bash
127
+ # .git/hooks/pre-push or CI script
143
128
 
144
- ```javascript
145
- // Coming in v2.0
146
- {
147
- channelConfig: {
148
- next: {
149
- labelPrefix: 'next:',
150
- addComment: false
151
- },
152
- latest: {
153
- labelPrefix: 'stable:',
154
- addComment: true
155
- }
156
- }
157
- }
129
+ branch=$(git rev-parse --abbrev-ref HEAD)
130
+ pattern="^(feature|bugfix|fix|hotfix)/[A-Z]+-[0-9]+|^[A-Z]+-[0-9]+"
131
+
132
+ if ! [[ "$branch" =~ $pattern ]]; then
133
+ echo "โŒ Branch name must include a Linear issue ID"
134
+ echo " Example: feature/ENG-123-description"
135
+ exit 1
136
+ fi
158
137
  ```
159
138
 
160
- ### Dry Run
139
+ ## How It Works
140
+
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
146
+
147
+ ### Label Colors
148
+
149
+ Labels are color-coded by release type:
161
150
 
162
- Test what issues would be updated without making changes:
151
+ - ๐Ÿ”ด **Major** (breaking changes) - Red
152
+ - ๐ŸŸ  **Minor** (new features) - Orange
153
+ - ๐ŸŸข **Patch** (bug fixes) - Green
154
+ - ๐ŸŸฃ **Prerelease** - Purple
155
+
156
+ ## Dry Run Mode
157
+
158
+ Test what will happen without making changes:
163
159
 
164
160
  ```javascript
165
161
  {
166
- dryRun: true
162
+ "plugins": [
163
+ ["semantic-release-linear", {
164
+ "dryRun": true
165
+ }]
166
+ ]
167
167
  }
168
168
  ```
169
169
 
170
+ Output:
171
+ ```
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
175
+ ```
176
+
170
177
  ## Linear API Setup
171
178
 
172
- 1. Go to Linear Settings โ†’ API โ†’ Personal API keys
173
- 2. Create a new key with "write" access
179
+ 1. Go to **Linear Settings** โ†’ **API** โ†’ **Personal API keys**
180
+ 2. Create a new key with **write** access
174
181
  3. Add to your CI environment as `LINEAR_API_KEY`
175
182
 
176
- ## CI Configuration
183
+ ## CI Examples
177
184
 
178
185
  ### GitHub Actions
179
186
 
180
187
  ```yaml
181
- - name: Release
182
- env:
183
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
184
- NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
185
- LINEAR_API_KEY: ${{ secrets.LINEAR_API_KEY }}
186
- run: npx semantic-release
188
+ name: Release
189
+
190
+ on:
191
+ push:
192
+ branches: [main, next]
193
+
194
+ jobs:
195
+ release:
196
+ runs-on: ubuntu-latest
197
+ steps:
198
+ - uses: actions/checkout@v3
199
+
200
+ - name: Setup Node.js
201
+ uses: actions/setup-node@v3
202
+ with:
203
+ node-version: 18
204
+
205
+ - name: Install dependencies
206
+ run: npm ci
207
+
208
+ - name: Release
209
+ env:
210
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
211
+ NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
212
+ LINEAR_API_KEY: ${{ secrets.LINEAR_API_KEY }}
213
+ run: npx semantic-release
187
214
  ```
188
215
 
216
+ ## FAQ
217
+
218
+ ### Why only branch names?
219
+
220
+ Extracting from commits is unreliable - developers forget, commits get squashed, and there's no enforcement. Branch names are:
221
+ - Required for every PR
222
+ - Easily enforced via Git hooks
223
+ - Visible in PR lists
224
+ - A single source of truth
225
+
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
+ ### Can I update multiple issues?
235
+
236
+ Yes, include multiple issue IDs in your branch name:
237
+ ```bash
238
+ feature/ENG-123-FEAT-456-big-feature
239
+ ```
240
+
241
+ ### What if my branch doesn't have an issue?
242
+
243
+ The plugin will skip it (unless `requireIssueInBranch: false`). This is intentional - not every release needs to update Linear.
244
+
189
245
  ## Troubleshooting
190
246
 
191
247
  ### Issues not being detected
192
248
 
193
- - Ensure issue IDs follow the format `TEAM-NUMBER` (e.g., `ENG-123`)
194
- - Check that team keys match if using the `teamKeys` filter
195
- - Verify the issues exist in Linear
249
+ 1. Check branch name: `git rev-parse --abbrev-ref HEAD`
250
+ 2. Verify format matches: `TEAM-NUMBER`
251
+ 3. Check team keys filter if configured
252
+ 4. Run with `dryRun: true` to debug
196
253
 
197
254
  ### API errors
198
255
 
199
- - Confirm your API key has write access
200
- - Check that the Linear workspace is accessible
201
- - Ensure network connectivity from CI environment
256
+ - Verify API key has write access
257
+ - Check Linear workspace permissions
258
+ - Ensure network connectivity in CI
202
259
 
203
260
  ## License
204
261
 
@@ -1,15 +1,18 @@
1
- import { Commit } from "semantic-release";
2
1
  /**
3
- * Extract Linear issue IDs from a commit
4
- * @param commit - The commit object from semantic-release
5
- * @param teamKeys - Optional list of team keys to filter by
6
- * @returns Set of unique issue identifiers
2
+ * Extract Linear issue IDs from branch name ONLY
3
+ * This enforces a single source of truth for issue tracking
7
4
  */
8
- export declare function parseCommit(commit: Commit, teamKeys?: string[] | null): Set<string>;
9
5
  /**
10
- * Extract all Linear issue IDs from a list of commits
11
- * @param commits - Array of commit objects
6
+ * Extract Linear issue IDs from a branch name
7
+ * @param branchName - The branch name to parse
12
8
  * @param teamKeys - Optional list of team keys to filter by
13
9
  * @returns Array of unique issue identifiers
14
10
  */
15
- export declare function parseIssues(commits: readonly Commit[], teamKeys?: string[] | null): string[];
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;
@@ -1,46 +1,48 @@
1
1
  "use strict";
2
+ /**
3
+ * Extract Linear issue IDs from branch name ONLY
4
+ * This enforces a single source of truth for issue tracking
5
+ */
2
6
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.parseCommit = parseCommit;
4
- exports.parseIssues = parseIssues;
7
+ exports.parseIssuesFromBranch = parseIssuesFromBranch;
8
+ exports.shouldProcessBranch = shouldProcessBranch;
5
9
  /**
6
- * Extract Linear issue IDs from a commit
7
- * @param commit - The commit object from semantic-release
10
+ * Extract Linear issue IDs from a branch name
11
+ * @param branchName - The branch name to parse
8
12
  * @param teamKeys - Optional list of team keys to filter by
9
- * @returns Set of unique issue identifiers
13
+ * @returns Array of unique issue identifiers
10
14
  */
11
- function parseCommit(commit, teamKeys = null) {
15
+ function parseIssuesFromBranch(branchName, teamKeys = null) {
12
16
  const issues = new Set();
13
17
  // Build regex pattern based on team keys
14
18
  const teamPattern = teamKeys ? `(?:${teamKeys.join("|")})` : "[A-Z]+";
15
- // Pattern matches: ENG-123, FEAT-45, etc.
19
+ // Pattern matches: feature/ENG-123-description, ENG-123, bugfix/FEAT-45, etc.
16
20
  const issuePattern = new RegExp(`\\b(${teamPattern}-\\d+)\\b`, "gi");
17
- // Search in commit message
18
- if (commit.message) {
19
- const messageMatches = Array.from(commit.message.matchAll(issuePattern));
20
- for (const match of messageMatches) {
21
- issues.add(match[1].toUpperCase());
22
- }
23
- }
24
- // Search in commit body
25
- if (commit.body) {
26
- const bodyMatches = Array.from(commit.body.matchAll(issuePattern));
27
- for (const match of bodyMatches) {
28
- issues.add(match[1].toUpperCase());
29
- }
21
+ const matches = Array.from(branchName.matchAll(issuePattern));
22
+ for (const match of matches) {
23
+ issues.add(match[1].toUpperCase());
30
24
  }
31
- return issues;
25
+ return Array.from(issues);
32
26
  }
33
27
  /**
34
- * Extract all Linear issue IDs from a list of commits
35
- * @param commits - Array of commit objects
36
- * @param teamKeys - Optional list of team keys to filter by
37
- * @returns Array of unique issue identifiers
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
38
32
  */
39
- function parseIssues(commits, teamKeys = null) {
40
- const allIssues = new Set();
41
- commits.forEach((commit) => {
42
- const commitIssues = parseCommit(commit, teamKeys);
43
- commitIssues.forEach((issue) => allIssues.add(issue));
44
- });
45
- return Array.from(allIssues);
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);
46
48
  }
@@ -2,22 +2,38 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const parse_issues_1 = require("./parse-issues");
4
4
  describe("parse-issues", () => {
5
- test("extracts Linear issue IDs from commits", () => {
6
- const commits = [
7
- { message: "fix: solve ENG-123 and FEAT-456" },
8
- {
9
- message: "feat: new feature",
10
- body: "Closes BUG-789",
11
- },
12
- { message: "chore: no issues here" },
13
- ];
14
- const result = (0, parse_issues_1.parseIssues)(commits);
15
- expect(result).toEqual(expect.arrayContaining(["ENG-123", "FEAT-456", "BUG-789"]));
16
- expect(result).toHaveLength(3);
5
+ test("extracts Linear issue IDs from branch name", () => {
6
+ const branchName = "feature/ENG-123-add-new-feature";
7
+ const result = (0, parse_issues_1.parseIssuesFromBranch)(branchName);
8
+ expect(result).toEqual(["ENG-123"]);
9
+ });
10
+ test("extracts multiple issue IDs from branch name", () => {
11
+ const branchName = "fix/ENG-123-FEAT-456-bug-fix";
12
+ const result = (0, parse_issues_1.parseIssuesFromBranch)(branchName);
13
+ expect(result).toEqual(expect.arrayContaining(["ENG-123", "FEAT-456"]));
14
+ expect(result).toHaveLength(2);
17
15
  });
18
16
  test("filters by team keys when provided", () => {
19
- const commits = [{ message: "fix: ENG-123 OTHER-456" }];
20
- const result = (0, parse_issues_1.parseIssues)(commits, ["ENG"]);
17
+ const branchName = "feature/ENG-123-OTHER-456";
18
+ const result = (0, parse_issues_1.parseIssuesFromBranch)(branchName, ["ENG"]);
21
19
  expect(result).toEqual(["ENG-123"]);
22
20
  });
21
+ test("returns empty array for branch without issues", () => {
22
+ const branchName = "feature/no-issues-here";
23
+ const result = (0, parse_issues_1.parseIssuesFromBranch)(branchName);
24
+ expect(result).toEqual([]);
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
+ });
23
39
  });
@@ -2,9 +2,14 @@ import { SuccessContext } from "semantic-release";
2
2
  import { PluginConfig, LinearContext } from "../types";
3
3
  interface ExtendedContext extends SuccessContext {
4
4
  linear?: LinearContext;
5
+ branch: {
6
+ name: string;
7
+ [key: string]: unknown;
8
+ };
5
9
  }
6
10
  /**
7
11
  * Update Linear issues after a successful release
12
+ * Only uses branch name for issue detection - single source of truth
8
13
  */
9
14
  export declare function success(pluginConfig: PluginConfig, context: ExtendedContext): Promise<void>;
10
15
  export {};
@@ -5,31 +5,45 @@ const linear_client_1 = require("./linear-client");
5
5
  const parse_issues_1 = require("./parse-issues");
6
6
  /**
7
7
  * Update Linear issues after a successful release
8
+ * Only uses branch name for issue detection - single source of truth
8
9
  */
9
10
  async function success(pluginConfig, context) {
10
- const { logger, nextRelease, commits, linear } = context;
11
+ const { logger, nextRelease, linear, branch } = context;
11
12
  if (!linear) {
12
13
  logger.log("Linear context not found, skipping issue updates");
13
14
  return;
14
15
  }
15
- const { removeOldLabels = true, addComment = false, dryRun = false, } = pluginConfig;
16
+ if (!branch.name) {
17
+ logger.log("No branch name available, skipping Linear updates");
18
+ return;
19
+ }
20
+ const { removeOldLabels = true, addComment = false, dryRun = false, skipBranches = ["main", "master", "develop", "staging", "production"], requireIssueInBranch = true, } = pluginConfig;
21
+ // Check if this branch should be processed
22
+ if (requireIssueInBranch && !(0, parse_issues_1.shouldProcessBranch)(branch.name, skipBranches)) {
23
+ logger.log(`Branch "${branch.name}" doesn't contain Linear issues or is in skip list, skipping updates`);
24
+ return;
25
+ }
16
26
  const client = new linear_client_1.LinearClient(linear.apiKey);
17
27
  const version = nextRelease.version;
18
28
  const channel = nextRelease.channel || "latest";
19
29
  // Format the label based on configuration
20
30
  const labelName = `${linear.labelPrefix}${version}`;
21
31
  const labelColor = getLabelColor(nextRelease.type);
22
- logger.log(`Updating Linear issues for release ${version} (${channel})`);
23
- // Extract issue IDs from commits
24
- const issueIds = (0, parse_issues_1.parseIssues)(commits, linear.teamKeys);
32
+ logger.log(`Updating Linear issues for release ${version} (${channel}) from branch "${branch.name}"`);
33
+ // Extract issue IDs from branch name ONLY
34
+ const issueIds = (0, parse_issues_1.parseIssuesFromBranch)(branch.name, linear.teamKeys);
25
35
  if (issueIds.length === 0) {
26
- logger.log("No Linear issues found in commits");
36
+ logger.log(`No Linear issues found in branch name "${branch.name}"`);
37
+ if (requireIssueInBranch) {
38
+ logger.warn("โš ๏ธ Consider using branch names like: feature/ENG-123-description");
39
+ }
27
40
  return;
28
41
  }
29
- logger.log(`Found ${issueIds.length} Linear issue(s): ${issueIds.join(", ")}`);
42
+ logger.log(`Found ${issueIds.length} Linear issue(s) in branch "${branch.name}": ${issueIds.join(", ")}`);
30
43
  if (dryRun) {
31
44
  logger.log("[Dry run] Would update issues:", issueIds);
32
45
  logger.log(`[Dry run] Would apply label: ${labelName}`);
46
+ logger.log(`[Dry run] Branch: ${branch.name}`);
33
47
  return;
34
48
  }
35
49
  // Ensure the version label exists
@@ -41,7 +55,7 @@ async function success(pluginConfig, context) {
41
55
  // Get the issue first
42
56
  const issue = await client.getIssue(issueId);
43
57
  if (!issue) {
44
- logger.warn(`Issue ${issueId} not found, skipping`);
58
+ logger.warn(`Issue ${issueId} not found in Linear, skipping`);
45
59
  return { issueId, status: "not_found" };
46
60
  }
47
61
  // Remove old version labels if configured
@@ -52,7 +66,7 @@ async function success(pluginConfig, context) {
52
66
  await client.addLabelToIssue(issue.id, label.id);
53
67
  // Add comment if configured
54
68
  if (addComment) {
55
- const comment = formatComment(version, channel, nextRelease);
69
+ const comment = formatComment(version, channel, nextRelease, branch.name);
56
70
  await client.addComment(issue.id, comment);
57
71
  }
58
72
  logger.log(`โœ“ Updated issue ${issueId}`);
@@ -89,10 +103,11 @@ function getLabelColor(releaseType) {
89
103
  /**
90
104
  * Format comment for Linear issue
91
105
  */
92
- function formatComment(version, channel, release) {
106
+ function formatComment(version, channel, release, branchName) {
93
107
  const emoji = channel === "latest" ? "๐Ÿš€" : "๐Ÿ”ฌ";
94
108
  const channelText = channel === "latest" ? "stable" : channel;
95
109
  let comment = `${emoji} **Released in \`v${version}\`** (${channelText})\n\n`;
110
+ comment += `๐Ÿ“Œ Released from branch: \`${branchName}\`\n\n`;
96
111
  const githubRepo = process.env.GITHUB_REPOSITORY;
97
112
  if (release.gitTag && githubRepo) {
98
113
  comment += `[View release โ†’](https://github.com/${githubRepo}/releases/tag/${release.gitTag})`;
package/dist/types.d.ts CHANGED
@@ -11,6 +11,10 @@ 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;
14
18
  }
15
19
  export interface LinearContext {
16
20
  apiKey: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "semantic-release-linear-app",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
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",
@@ -1,28 +1,49 @@
1
- import { parseIssues } from "./parse-issues";
2
- import { Commit } from "semantic-release";
1
+ import { parseIssuesFromBranch, shouldProcessBranch } from "./parse-issues";
3
2
 
4
3
  describe("parse-issues", () => {
5
- test("extracts Linear issue IDs from commits", () => {
6
- const commits = [
7
- { message: "fix: solve ENG-123 and FEAT-456" },
8
- {
9
- message: "feat: new feature",
10
- body: "Closes BUG-789",
11
- },
12
- { message: "chore: no issues here" },
13
- ] as Commit[];
14
-
15
- const result = parseIssues(commits);
16
- expect(result).toEqual(
17
- expect.arrayContaining(["ENG-123", "FEAT-456", "BUG-789"]),
18
- );
19
- expect(result).toHaveLength(3);
4
+ test("extracts Linear issue IDs from branch name", () => {
5
+ const branchName = "feature/ENG-123-add-new-feature";
6
+ const result = parseIssuesFromBranch(branchName);
7
+ expect(result).toEqual(["ENG-123"]);
20
8
  });
21
9
 
22
- test("filters by team keys when provided", () => {
23
- const commits = [{ message: "fix: ENG-123 OTHER-456" }] as Commit[];
10
+ test("extracts multiple issue IDs from branch name", () => {
11
+ const branchName = "fix/ENG-123-FEAT-456-bug-fix";
12
+ const result = parseIssuesFromBranch(branchName);
13
+ expect(result).toEqual(expect.arrayContaining(["ENG-123", "FEAT-456"]));
14
+ expect(result).toHaveLength(2);
15
+ });
24
16
 
25
- const result = parseIssues(commits, ["ENG"]);
17
+ test("filters by team keys when provided", () => {
18
+ const branchName = "feature/ENG-123-OTHER-456";
19
+ const result = parseIssuesFromBranch(branchName, ["ENG"]);
26
20
  expect(result).toEqual(["ENG-123"]);
27
21
  });
22
+
23
+ test("returns empty array for branch without issues", () => {
24
+ const branchName = "feature/no-issues-here";
25
+ const result = parseIssuesFromBranch(branchName);
26
+ expect(result).toEqual([]);
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
+ });
28
49
  });
@@ -1,58 +1,57 @@
1
- import { Commit } from "semantic-release";
1
+ /**
2
+ * Extract Linear issue IDs from branch name ONLY
3
+ * This enforces a single source of truth for issue tracking
4
+ */
2
5
 
3
6
  /**
4
- * Extract Linear issue IDs from a commit
5
- * @param commit - The commit object from semantic-release
7
+ * Extract Linear issue IDs from a branch name
8
+ * @param branchName - The branch name to parse
6
9
  * @param teamKeys - Optional list of team keys to filter by
7
- * @returns Set of unique issue identifiers
10
+ * @returns Array of unique issue identifiers
8
11
  */
9
- export function parseCommit(
10
- commit: Commit,
12
+ export function parseIssuesFromBranch(
13
+ branchName: string,
11
14
  teamKeys: string[] | null = null,
12
- ): Set<string> {
15
+ ): string[] {
13
16
  const issues = new Set<string>();
14
17
 
15
18
  // Build regex pattern based on team keys
16
19
  const teamPattern = teamKeys ? `(?:${teamKeys.join("|")})` : "[A-Z]+";
17
20
 
18
- // Pattern matches: ENG-123, FEAT-45, etc.
21
+ // Pattern matches: feature/ENG-123-description, ENG-123, bugfix/FEAT-45, etc.
19
22
  const issuePattern = new RegExp(`\\b(${teamPattern}-\\d+)\\b`, "gi");
20
23
 
21
- // Search in commit message
22
- if (commit.message) {
23
- const messageMatches = Array.from(commit.message.matchAll(issuePattern));
24
- for (const match of messageMatches) {
25
- issues.add(match[1].toUpperCase());
26
- }
27
- }
28
-
29
- // Search in commit body
30
- if (commit.body) {
31
- const bodyMatches = Array.from(commit.body.matchAll(issuePattern));
32
- for (const match of bodyMatches) {
33
- issues.add(match[1].toUpperCase());
34
- }
24
+ const matches = Array.from(branchName.matchAll(issuePattern));
25
+ for (const match of matches) {
26
+ issues.add(match[1].toUpperCase());
35
27
  }
36
28
 
37
- return issues;
29
+ return Array.from(issues);
38
30
  }
39
31
 
40
32
  /**
41
- * Extract all Linear issue IDs from a list of commits
42
- * @param commits - Array of commit objects
43
- * @param teamKeys - Optional list of team keys to filter by
44
- * @returns Array of unique issue identifiers
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
45
37
  */
46
- export function parseIssues(
47
- commits: readonly Commit[],
48
- teamKeys: string[] | null = null,
49
- ): string[] {
50
- const allIssues = new Set<string>();
51
-
52
- commits.forEach((commit) => {
53
- const commitIssues = parseCommit(commit, teamKeys);
54
- commitIssues.forEach((issue) => allIssues.add(issue));
55
- });
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
+ }
56
54
 
57
- return Array.from(allIssues);
55
+ // Process any other branch that contains an issue ID
56
+ return /[A-Z]+-\d+/.test(branchName);
58
57
  }
@@ -1,6 +1,6 @@
1
1
  import { SuccessContext } from "semantic-release";
2
2
  import { LinearClient } from "./linear-client";
3
- import { parseIssues } from "./parse-issues";
3
+ import { parseIssuesFromBranch, shouldProcessBranch } from "./parse-issues";
4
4
  import {
5
5
  PluginConfig,
6
6
  LinearContext,
@@ -10,28 +10,48 @@ import {
10
10
 
11
11
  interface ExtendedContext extends SuccessContext {
12
12
  linear?: LinearContext;
13
+ branch: {
14
+ name: string;
15
+ [key: string]: unknown;
16
+ };
13
17
  }
14
18
 
15
19
  /**
16
20
  * Update Linear issues after a successful release
21
+ * Only uses branch name for issue detection - single source of truth
17
22
  */
18
23
  export async function success(
19
24
  pluginConfig: PluginConfig,
20
25
  context: ExtendedContext,
21
26
  ): Promise<void> {
22
- const { logger, nextRelease, commits, linear } = context;
27
+ const { logger, nextRelease, linear, branch } = context;
23
28
 
24
29
  if (!linear) {
25
30
  logger.log("Linear context not found, skipping issue updates");
26
31
  return;
27
32
  }
28
33
 
34
+ if (!branch.name) {
35
+ logger.log("No branch name available, skipping Linear updates");
36
+ return;
37
+ }
38
+
29
39
  const {
30
40
  removeOldLabels = true,
31
41
  addComment = false,
32
42
  dryRun = false,
43
+ skipBranches = ["main", "master", "develop", "staging", "production"],
44
+ requireIssueInBranch = true,
33
45
  } = pluginConfig;
34
46
 
47
+ // Check if this branch should be processed
48
+ if (requireIssueInBranch && !shouldProcessBranch(branch.name, skipBranches)) {
49
+ logger.log(
50
+ `Branch "${branch.name}" doesn't contain Linear issues or is in skip list, skipping updates`,
51
+ );
52
+ return;
53
+ }
54
+
35
55
  const client = new LinearClient(linear.apiKey);
36
56
  const version = nextRelease.version;
37
57
  const channel = nextRelease.channel || "latest";
@@ -40,23 +60,31 @@ export async function success(
40
60
  const labelName = `${linear.labelPrefix}${version}`;
41
61
  const labelColor = getLabelColor(nextRelease.type as ReleaseType);
42
62
 
43
- logger.log(`Updating Linear issues for release ${version} (${channel})`);
63
+ logger.log(
64
+ `Updating Linear issues for release ${version} (${channel}) from branch "${branch.name}"`,
65
+ );
44
66
 
45
- // Extract issue IDs from commits
46
- const issueIds = parseIssues(commits, linear.teamKeys);
67
+ // Extract issue IDs from branch name ONLY
68
+ const issueIds = parseIssuesFromBranch(branch.name, linear.teamKeys);
47
69
 
48
70
  if (issueIds.length === 0) {
49
- logger.log("No Linear issues found in commits");
71
+ logger.log(`No Linear issues found in branch name "${branch.name}"`);
72
+ if (requireIssueInBranch) {
73
+ logger.warn(
74
+ "โš ๏ธ Consider using branch names like: feature/ENG-123-description",
75
+ );
76
+ }
50
77
  return;
51
78
  }
52
79
 
53
80
  logger.log(
54
- `Found ${issueIds.length} Linear issue(s): ${issueIds.join(", ")}`,
81
+ `Found ${issueIds.length} Linear issue(s) in branch "${branch.name}": ${issueIds.join(", ")}`,
55
82
  );
56
83
 
57
84
  if (dryRun) {
58
85
  logger.log("[Dry run] Would update issues:", issueIds);
59
86
  logger.log(`[Dry run] Would apply label: ${labelName}`);
87
+ logger.log(`[Dry run] Branch: ${branch.name}`);
60
88
  return;
61
89
  }
62
90
 
@@ -72,7 +100,7 @@ export async function success(
72
100
  const issue = await client.getIssue(issueId);
73
101
 
74
102
  if (!issue) {
75
- logger.warn(`Issue ${issueId} not found, skipping`);
103
+ logger.warn(`Issue ${issueId} not found in Linear, skipping`);
76
104
  return { issueId, status: "not_found" };
77
105
  }
78
106
 
@@ -86,7 +114,12 @@ export async function success(
86
114
 
87
115
  // Add comment if configured
88
116
  if (addComment) {
89
- const comment = formatComment(version, channel, nextRelease);
117
+ const comment = formatComment(
118
+ version,
119
+ channel,
120
+ nextRelease,
121
+ branch.name,
122
+ );
90
123
  await client.addComment(issue.id, comment);
91
124
  }
92
125
 
@@ -144,11 +177,13 @@ function formatComment(
144
177
  version: string,
145
178
  channel: string,
146
179
  release: SuccessContext["nextRelease"],
180
+ branchName: string,
147
181
  ): string {
148
182
  const emoji = channel === "latest" ? "๐Ÿš€" : "๐Ÿ”ฌ";
149
183
  const channelText = channel === "latest" ? "stable" : channel;
150
184
 
151
185
  let comment = `${emoji} **Released in \`v${version}\`** (${channelText})\n\n`;
186
+ comment += `๐Ÿ“Œ Released from branch: \`${branchName}\`\n\n`;
152
187
 
153
188
  const githubRepo = process.env.GITHUB_REPOSITORY;
154
189
  if (release.gitTag && githubRepo) {
package/src/types.ts CHANGED
@@ -16,6 +16,12 @@ 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;
19
25
  }
20
26
 
21
27
  export interface LinearContext {