semantic-release-linear-app 0.1.0-next.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +205 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +11 -0
- package/dist/lib/linear-client.d.ts +39 -0
- package/dist/lib/linear-client.js +191 -0
- package/dist/lib/parse-issues.d.ts +15 -0
- package/dist/lib/parse-issues.js +46 -0
- package/dist/lib/parse-issues.test.d.ts +1 -0
- package/dist/lib/parse-issues.test.js +23 -0
- package/dist/lib/success.d.ts +10 -0
- package/dist/lib/success.js +101 -0
- package/dist/lib/verify.d.ts +8 -0
- package/dist/lib/verify.js +40 -0
- package/dist/lib/verify.test.d.ts +1 -0
- package/dist/lib/verify.test.js +30 -0
- package/dist/types.d.ts +42 -0
- package/dist/types.js +2 -0
- package/package.json +84 -0
- package/src/index.ts +12 -0
- package/src/lib/linear-client.ts +240 -0
- package/src/lib/parse-issues.test.ts +28 -0
- package/src/lib/parse-issues.ts +58 -0
- package/src/lib/success.ts +159 -0
- package/src/lib/verify.test.ts +39 -0
- package/src/lib/verify.ts +57 -0
- package/src/types/semantic-release-error.d.ts +7 -0
- package/src/types.ts +60 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Caio Pizzol
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# semantic-release-linear-app
|
|
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.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
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
|
|
12
|
+
- โก Batch operations for efficiency
|
|
13
|
+
- ๐ Full TypeScript support with type definitions
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install --save-dev semantic-release-linear-app
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
Add the plugin to your semantic-release configuration:
|
|
24
|
+
|
|
25
|
+
```json
|
|
26
|
+
{
|
|
27
|
+
"plugins": [
|
|
28
|
+
"@semantic-release/commit-analyzer",
|
|
29
|
+
"@semantic-release/release-notes-generator",
|
|
30
|
+
["semantic-release-linear-app", {
|
|
31
|
+
"apiKey": "lin_api_xxx"
|
|
32
|
+
}],
|
|
33
|
+
"@semantic-release/github"
|
|
34
|
+
]
|
|
35
|
+
}
|
|
36
|
+
```
|
|
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';
|
|
44
|
+
|
|
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
|
+
};
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Configuration
|
|
55
|
+
|
|
56
|
+
### Authentication
|
|
57
|
+
|
|
58
|
+
Set your Linear API key via environment variable:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
export LINEAR_API_KEY=lin_api_xxx
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Or pass it in the plugin configuration:
|
|
65
|
+
|
|
66
|
+
```json
|
|
67
|
+
{
|
|
68
|
+
"apiKey": "lin_api_xxx"
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Options
|
|
73
|
+
|
|
74
|
+
| Option | Default | Description |
|
|
75
|
+
|--------|---------|-------------|
|
|
76
|
+
| `apiKey` | - | Linear API key (or use `LINEAR_API_KEY` env var) |
|
|
77
|
+
| `teamKeys` | `[]` | Array of team keys to filter issues (e.g., `["ENG", "FEAT"]`) |
|
|
78
|
+
| `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 |
|
|
82
|
+
|
|
83
|
+
### Example Configuration
|
|
84
|
+
|
|
85
|
+
```javascript
|
|
86
|
+
// .releaserc.js
|
|
87
|
+
module.exports = {
|
|
88
|
+
branches: ['main', 'next'],
|
|
89
|
+
plugins: [
|
|
90
|
+
'@semantic-release/commit-analyzer',
|
|
91
|
+
'@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',
|
|
99
|
+
'@semantic-release/github'
|
|
100
|
+
]
|
|
101
|
+
};
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## How It Works
|
|
105
|
+
|
|
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:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
# In commit message
|
|
117
|
+
git commit -m "feat: add new feature ENG-123"
|
|
118
|
+
|
|
119
|
+
# In commit body
|
|
120
|
+
git commit -m "feat: add new feature" -m "Closes ENG-123"
|
|
121
|
+
|
|
122
|
+
# Multiple issues
|
|
123
|
+
git commit -m "fix: resolve bugs ENG-123, FEAT-456"
|
|
124
|
+
|
|
125
|
+
# In PR title (when squash merging)
|
|
126
|
+
"feat: add feature (#123) ENG-456"
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Label Colors
|
|
130
|
+
|
|
131
|
+
Labels are automatically color-coded based on the release type:
|
|
132
|
+
|
|
133
|
+
- ๐ด **Major** releases (breaking changes) - Red
|
|
134
|
+
- ๐ **Minor** releases (new features) - Orange
|
|
135
|
+
- ๐ข **Patch** releases (bug fixes) - Green
|
|
136
|
+
- ๐ฃ **Prerelease** versions - Purple
|
|
137
|
+
|
|
138
|
+
## Advanced Usage
|
|
139
|
+
|
|
140
|
+
### Channel-Specific Configuration
|
|
141
|
+
|
|
142
|
+
For different behavior on different release channels:
|
|
143
|
+
|
|
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
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Dry Run
|
|
161
|
+
|
|
162
|
+
Test what issues would be updated without making changes:
|
|
163
|
+
|
|
164
|
+
```javascript
|
|
165
|
+
{
|
|
166
|
+
dryRun: true
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## Linear API Setup
|
|
171
|
+
|
|
172
|
+
1. Go to Linear Settings โ API โ Personal API keys
|
|
173
|
+
2. Create a new key with "write" access
|
|
174
|
+
3. Add to your CI environment as `LINEAR_API_KEY`
|
|
175
|
+
|
|
176
|
+
## CI Configuration
|
|
177
|
+
|
|
178
|
+
### GitHub Actions
|
|
179
|
+
|
|
180
|
+
```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
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## Troubleshooting
|
|
190
|
+
|
|
191
|
+
### Issues not being detected
|
|
192
|
+
|
|
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
|
|
196
|
+
|
|
197
|
+
### API errors
|
|
198
|
+
|
|
199
|
+
- Confirm your API key has write access
|
|
200
|
+
- Check that the Linear workspace is accessible
|
|
201
|
+
- Ensure network connectivity from CI environment
|
|
202
|
+
|
|
203
|
+
## License
|
|
204
|
+
|
|
205
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* semantic-release-linear-app
|
|
3
|
+
* A semantic-release plugin to update Linear issues with version labels
|
|
4
|
+
*/
|
|
5
|
+
import { verifyConditions } from "./lib/verify";
|
|
6
|
+
import { success } from "./lib/success";
|
|
7
|
+
export { verifyConditions, success };
|
|
8
|
+
export type { PluginConfig } from "./types";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* semantic-release-linear-app
|
|
4
|
+
* A semantic-release plugin to update Linear issues with version labels
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.success = exports.verifyConditions = void 0;
|
|
8
|
+
const verify_1 = require("./lib/verify");
|
|
9
|
+
Object.defineProperty(exports, "verifyConditions", { enumerable: true, get: function () { return verify_1.verifyConditions; } });
|
|
10
|
+
const success_1 = require("./lib/success");
|
|
11
|
+
Object.defineProperty(exports, "success", { enumerable: true, get: function () { return success_1.success; } });
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { LinearIssue, LinearLabel, LinearViewer } from "../types";
|
|
2
|
+
/**
|
|
3
|
+
* Linear API client for GraphQL operations
|
|
4
|
+
*/
|
|
5
|
+
export declare class LinearClient {
|
|
6
|
+
private apiKey;
|
|
7
|
+
private apiUrl;
|
|
8
|
+
constructor(apiKey: string);
|
|
9
|
+
/**
|
|
10
|
+
* Execute a GraphQL query
|
|
11
|
+
*/
|
|
12
|
+
private query;
|
|
13
|
+
/**
|
|
14
|
+
* Test the API connection
|
|
15
|
+
*/
|
|
16
|
+
testConnection(): Promise<LinearViewer>;
|
|
17
|
+
/**
|
|
18
|
+
* Find or create a label
|
|
19
|
+
*/
|
|
20
|
+
ensureLabel(name: string, color?: string): Promise<LinearLabel>;
|
|
21
|
+
/**
|
|
22
|
+
* Get issue by identifier
|
|
23
|
+
*/
|
|
24
|
+
getIssue(identifier: string): Promise<LinearIssue | null>;
|
|
25
|
+
/**
|
|
26
|
+
* Add label to issue
|
|
27
|
+
*/
|
|
28
|
+
addLabelToIssue(issueId: string, labelId: string): Promise<LinearIssue>;
|
|
29
|
+
/**
|
|
30
|
+
* Remove old version labels from issue
|
|
31
|
+
*/
|
|
32
|
+
removeVersionLabels(issueId: string, labelPrefix: string): Promise<LinearIssue | null>;
|
|
33
|
+
/**
|
|
34
|
+
* Add comment to issue
|
|
35
|
+
*/
|
|
36
|
+
addComment(issueId: string, body: string): Promise<{
|
|
37
|
+
id: string;
|
|
38
|
+
}>;
|
|
39
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.LinearClient = void 0;
|
|
7
|
+
const node_fetch_1 = __importDefault(require("node-fetch"));
|
|
8
|
+
/**
|
|
9
|
+
* Linear API client for GraphQL operations
|
|
10
|
+
*/
|
|
11
|
+
class LinearClient {
|
|
12
|
+
apiKey;
|
|
13
|
+
apiUrl = "https://api.linear.app/graphql";
|
|
14
|
+
constructor(apiKey) {
|
|
15
|
+
this.apiKey = apiKey;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Execute a GraphQL query
|
|
19
|
+
*/
|
|
20
|
+
async query(query, variables = {}) {
|
|
21
|
+
const response = await (0, node_fetch_1.default)(this.apiUrl, {
|
|
22
|
+
method: "POST",
|
|
23
|
+
headers: {
|
|
24
|
+
Authorization: this.apiKey,
|
|
25
|
+
"Content-Type": "application/json",
|
|
26
|
+
},
|
|
27
|
+
body: JSON.stringify({ query, variables }),
|
|
28
|
+
});
|
|
29
|
+
const data = (await response.json());
|
|
30
|
+
if (data.errors) {
|
|
31
|
+
throw new Error(`Linear API error: ${data.errors[0].message}`);
|
|
32
|
+
}
|
|
33
|
+
if (!data.data) {
|
|
34
|
+
throw new Error("No data returned from Linear API");
|
|
35
|
+
}
|
|
36
|
+
return data.data;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Test the API connection
|
|
40
|
+
*/
|
|
41
|
+
async testConnection() {
|
|
42
|
+
const query = `
|
|
43
|
+
query TestConnection {
|
|
44
|
+
viewer {
|
|
45
|
+
id
|
|
46
|
+
name
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
`;
|
|
50
|
+
const data = await this.query(query);
|
|
51
|
+
return data.viewer;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Find or create a label
|
|
55
|
+
*/
|
|
56
|
+
async ensureLabel(name, color = "#4752C4") {
|
|
57
|
+
// First, try to find existing label
|
|
58
|
+
const searchQuery = `
|
|
59
|
+
query FindLabel($name: String!) {
|
|
60
|
+
issueLabels(filter: { name: { eq: $name } }) {
|
|
61
|
+
nodes {
|
|
62
|
+
id
|
|
63
|
+
name
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
`;
|
|
68
|
+
const searchData = await this.query(searchQuery, { name });
|
|
69
|
+
if (searchData.issueLabels.nodes.length > 0) {
|
|
70
|
+
return searchData.issueLabels.nodes[0];
|
|
71
|
+
}
|
|
72
|
+
// Create new label if it doesn't exist
|
|
73
|
+
const createMutation = `
|
|
74
|
+
mutation CreateLabel($name: String!, $color: String!) {
|
|
75
|
+
issueLabelCreate(input: { name: $name, color: $color }) {
|
|
76
|
+
issueLabel {
|
|
77
|
+
id
|
|
78
|
+
name
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
`;
|
|
83
|
+
const createData = await this.query(createMutation, { name, color });
|
|
84
|
+
return createData.issueLabelCreate.issueLabel;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Get issue by identifier
|
|
88
|
+
*/
|
|
89
|
+
async getIssue(identifier) {
|
|
90
|
+
const query = `
|
|
91
|
+
query GetIssue($identifier: String!) {
|
|
92
|
+
issue(id: $identifier) {
|
|
93
|
+
id
|
|
94
|
+
identifier
|
|
95
|
+
title
|
|
96
|
+
labels {
|
|
97
|
+
nodes {
|
|
98
|
+
id
|
|
99
|
+
name
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
`;
|
|
105
|
+
try {
|
|
106
|
+
const data = await this.query(query, {
|
|
107
|
+
identifier,
|
|
108
|
+
});
|
|
109
|
+
return data.issue;
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
// Issue might not exist, return null
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Add label to issue
|
|
118
|
+
*/
|
|
119
|
+
async addLabelToIssue(issueId, labelId) {
|
|
120
|
+
const mutation = `
|
|
121
|
+
mutation AddLabel($issueId: String!, $labelId: String!) {
|
|
122
|
+
issueUpdate(
|
|
123
|
+
id: $issueId,
|
|
124
|
+
input: { labelIds: [$labelId] }
|
|
125
|
+
) {
|
|
126
|
+
issue {
|
|
127
|
+
id
|
|
128
|
+
identifier
|
|
129
|
+
title
|
|
130
|
+
labels {
|
|
131
|
+
nodes {
|
|
132
|
+
id
|
|
133
|
+
name
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
`;
|
|
140
|
+
const data = await this.query(mutation, { issueId, labelId });
|
|
141
|
+
return data.issueUpdate.issue;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Remove old version labels from issue
|
|
145
|
+
*/
|
|
146
|
+
async removeVersionLabels(issueId, labelPrefix) {
|
|
147
|
+
const issue = await this.getIssue(issueId);
|
|
148
|
+
if (!issue)
|
|
149
|
+
return null;
|
|
150
|
+
const versionLabels = issue.labels.nodes.filter((label) => label.name.startsWith(labelPrefix));
|
|
151
|
+
if (versionLabels.length === 0)
|
|
152
|
+
return issue;
|
|
153
|
+
const mutation = `
|
|
154
|
+
mutation RemoveLabels($issueId: String!, $labelIds: [String!]!) {
|
|
155
|
+
issueRemoveLabel(id: $issueId, labelIds: $labelIds) {
|
|
156
|
+
issue {
|
|
157
|
+
id
|
|
158
|
+
identifier
|
|
159
|
+
title
|
|
160
|
+
labels {
|
|
161
|
+
nodes {
|
|
162
|
+
id
|
|
163
|
+
name
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
`;
|
|
170
|
+
const labelIds = versionLabels.map((label) => label.id);
|
|
171
|
+
const data = await this.query(mutation, { issueId, labelIds });
|
|
172
|
+
return data.issueRemoveLabel.issue;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Add comment to issue
|
|
176
|
+
*/
|
|
177
|
+
async addComment(issueId, body) {
|
|
178
|
+
const mutation = `
|
|
179
|
+
mutation AddComment($issueId: String!, $body: String!) {
|
|
180
|
+
commentCreate(input: { issueId: $issueId, body: $body }) {
|
|
181
|
+
comment {
|
|
182
|
+
id
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
`;
|
|
187
|
+
const data = await this.query(mutation, { issueId, body });
|
|
188
|
+
return data.commentCreate.comment;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
exports.LinearClient = LinearClient;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Commit } from "semantic-release";
|
|
2
|
+
/**
|
|
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
|
|
7
|
+
*/
|
|
8
|
+
export declare function parseCommit(commit: Commit, teamKeys?: string[] | null): Set<string>;
|
|
9
|
+
/**
|
|
10
|
+
* Extract all Linear issue IDs from a list of commits
|
|
11
|
+
* @param commits - Array of commit objects
|
|
12
|
+
* @param teamKeys - Optional list of team keys to filter by
|
|
13
|
+
* @returns Array of unique issue identifiers
|
|
14
|
+
*/
|
|
15
|
+
export declare function parseIssues(commits: readonly Commit[], teamKeys?: string[] | null): string[];
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseCommit = parseCommit;
|
|
4
|
+
exports.parseIssues = parseIssues;
|
|
5
|
+
/**
|
|
6
|
+
* Extract Linear issue IDs from a commit
|
|
7
|
+
* @param commit - The commit object from semantic-release
|
|
8
|
+
* @param teamKeys - Optional list of team keys to filter by
|
|
9
|
+
* @returns Set of unique issue identifiers
|
|
10
|
+
*/
|
|
11
|
+
function parseCommit(commit, teamKeys = null) {
|
|
12
|
+
const issues = new Set();
|
|
13
|
+
// Build regex pattern based on team keys
|
|
14
|
+
const teamPattern = teamKeys ? `(?:${teamKeys.join("|")})` : "[A-Z]+";
|
|
15
|
+
// Pattern matches: ENG-123, FEAT-45, etc.
|
|
16
|
+
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
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return issues;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
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
|
|
38
|
+
*/
|
|
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);
|
|
46
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const parse_issues_1 = require("./parse-issues");
|
|
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);
|
|
17
|
+
});
|
|
18
|
+
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"]);
|
|
21
|
+
expect(result).toEqual(["ENG-123"]);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { SuccessContext } from "semantic-release";
|
|
2
|
+
import { PluginConfig, LinearContext } from "../types";
|
|
3
|
+
interface ExtendedContext extends SuccessContext {
|
|
4
|
+
linear?: LinearContext;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Update Linear issues after a successful release
|
|
8
|
+
*/
|
|
9
|
+
export declare function success(pluginConfig: PluginConfig, context: ExtendedContext): Promise<void>;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.success = success;
|
|
4
|
+
const linear_client_1 = require("./linear-client");
|
|
5
|
+
const parse_issues_1 = require("./parse-issues");
|
|
6
|
+
/**
|
|
7
|
+
* Update Linear issues after a successful release
|
|
8
|
+
*/
|
|
9
|
+
async function success(pluginConfig, context) {
|
|
10
|
+
const { logger, nextRelease, commits, linear } = context;
|
|
11
|
+
if (!linear) {
|
|
12
|
+
logger.log("Linear context not found, skipping issue updates");
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
const { removeOldLabels = true, addComment = false, dryRun = false, } = pluginConfig;
|
|
16
|
+
const client = new linear_client_1.LinearClient(linear.apiKey);
|
|
17
|
+
const version = nextRelease.version;
|
|
18
|
+
const channel = nextRelease.channel || "latest";
|
|
19
|
+
// Format the label based on configuration
|
|
20
|
+
const labelName = `${linear.labelPrefix}${version}`;
|
|
21
|
+
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);
|
|
25
|
+
if (issueIds.length === 0) {
|
|
26
|
+
logger.log("No Linear issues found in commits");
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
logger.log(`Found ${issueIds.length} Linear issue(s): ${issueIds.join(", ")}`);
|
|
30
|
+
if (dryRun) {
|
|
31
|
+
logger.log("[Dry run] Would update issues:", issueIds);
|
|
32
|
+
logger.log(`[Dry run] Would apply label: ${labelName}`);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
// Ensure the version label exists
|
|
36
|
+
const label = await client.ensureLabel(labelName, labelColor);
|
|
37
|
+
logger.log(`โ Ensured label exists: ${labelName}`);
|
|
38
|
+
// Update each issue
|
|
39
|
+
const results = await Promise.allSettled(issueIds.map(async (issueId) => {
|
|
40
|
+
try {
|
|
41
|
+
// Get the issue first
|
|
42
|
+
const issue = await client.getIssue(issueId);
|
|
43
|
+
if (!issue) {
|
|
44
|
+
logger.warn(`Issue ${issueId} not found, skipping`);
|
|
45
|
+
return { issueId, status: "not_found" };
|
|
46
|
+
}
|
|
47
|
+
// Remove old version labels if configured
|
|
48
|
+
if (removeOldLabels) {
|
|
49
|
+
await client.removeVersionLabels(issue.id, linear.labelPrefix);
|
|
50
|
+
}
|
|
51
|
+
// Add the new version label
|
|
52
|
+
await client.addLabelToIssue(issue.id, label.id);
|
|
53
|
+
// Add comment if configured
|
|
54
|
+
if (addComment) {
|
|
55
|
+
const comment = formatComment(version, channel, nextRelease);
|
|
56
|
+
await client.addComment(issue.id, comment);
|
|
57
|
+
}
|
|
58
|
+
logger.log(`โ Updated issue ${issueId}`);
|
|
59
|
+
return { issueId, status: "updated" };
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
63
|
+
logger.error(`Failed to update issue ${issueId}: ${message}`);
|
|
64
|
+
return { issueId, status: "failed", error: message };
|
|
65
|
+
}
|
|
66
|
+
}));
|
|
67
|
+
// Log summary
|
|
68
|
+
const updated = results.filter((r) => r.status === "fulfilled" && r.value.status === "updated").length;
|
|
69
|
+
const failed = results.filter((r) => r.status === "rejected" ||
|
|
70
|
+
(r.status === "fulfilled" && r.value.status === "failed")).length;
|
|
71
|
+
const notFound = results.filter((r) => r.status === "fulfilled" && r.value.status === "not_found").length;
|
|
72
|
+
logger.log(`Linear update complete: ${updated} updated, ${failed} failed, ${notFound} not found`);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Get label color based on release type
|
|
76
|
+
*/
|
|
77
|
+
function getLabelColor(releaseType) {
|
|
78
|
+
const colors = {
|
|
79
|
+
major: "#F44336", // Red for breaking changes
|
|
80
|
+
premajor: "#E91E63", // Pink for pre-major
|
|
81
|
+
minor: "#FF9800", // Orange for new features
|
|
82
|
+
preminor: "#FFC107", // Amber for pre-minor
|
|
83
|
+
patch: "#4CAF50", // Green for fixes
|
|
84
|
+
prepatch: "#8BC34A", // Light green for pre-patch
|
|
85
|
+
prerelease: "#9C27B0", // Purple for prereleases
|
|
86
|
+
};
|
|
87
|
+
return colors[releaseType] || "#4752C4"; // Default blue
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Format comment for Linear issue
|
|
91
|
+
*/
|
|
92
|
+
function formatComment(version, channel, release) {
|
|
93
|
+
const emoji = channel === "latest" ? "๐" : "๐ฌ";
|
|
94
|
+
const channelText = channel === "latest" ? "stable" : channel;
|
|
95
|
+
let comment = `${emoji} **Released in \`v${version}\`** (${channelText})\n\n`;
|
|
96
|
+
const githubRepo = process.env.GITHUB_REPOSITORY;
|
|
97
|
+
if (release.gitTag && githubRepo) {
|
|
98
|
+
comment += `[View release โ](https://github.com/${githubRepo}/releases/tag/${release.gitTag})`;
|
|
99
|
+
}
|
|
100
|
+
return comment;
|
|
101
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { VerifyConditionsContext } from "semantic-release";
|
|
2
|
+
import { PluginConfig, LinearContext } from "../types";
|
|
3
|
+
/**
|
|
4
|
+
* Verify the plugin configuration and Linear API access
|
|
5
|
+
*/
|
|
6
|
+
export declare function verifyConditions(pluginConfig: PluginConfig, context: VerifyConditionsContext & {
|
|
7
|
+
linear?: LinearContext;
|
|
8
|
+
}): Promise<void>;
|