semantic-release-linear-app 0.2.0 → 0.3.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/dist/lib/success.d.ts +0 -5
- package/dist/lib/success.js +69 -49
- package/dist/lib/verify.js +6 -2
- package/dist/lib/verify.test.js +12 -0
- package/package.json +2 -1
- package/src/lib/success.ts +85 -88
- package/src/lib/verify.test.ts +19 -0
- package/src/lib/verify.ts +9 -2
package/dist/lib/success.d.ts
CHANGED
|
@@ -2,14 +2,9 @@ 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
|
-
};
|
|
9
5
|
}
|
|
10
6
|
/**
|
|
11
7
|
* Update Linear issues after a successful release
|
|
12
|
-
* Only uses branch name for issue detection - single source of truth
|
|
13
8
|
*/
|
|
14
9
|
export declare function success(pluginConfig: PluginConfig, context: ExtendedContext): Promise<void>;
|
|
15
10
|
export {};
|
package/dist/lib/success.js
CHANGED
|
@@ -1,61 +1,94 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.success = success;
|
|
4
|
+
const execa_1 = require("execa");
|
|
4
5
|
const linear_client_1 = require("./linear-client");
|
|
5
6
|
const parse_issues_1 = require("./parse-issues");
|
|
7
|
+
/**
|
|
8
|
+
* Find source branches that contain the given commits
|
|
9
|
+
*/
|
|
10
|
+
async function findSourceBranches(commits, logger) {
|
|
11
|
+
const branches = new Set();
|
|
12
|
+
if (commits.length === 0)
|
|
13
|
+
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]}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
logger.debug(`Git branch lookup failed: ${error.message}`);
|
|
37
|
+
}
|
|
38
|
+
return branches;
|
|
39
|
+
}
|
|
6
40
|
/**
|
|
7
41
|
* Update Linear issues after a successful release
|
|
8
|
-
* Only uses branch name for issue detection - single source of truth
|
|
9
42
|
*/
|
|
10
43
|
async function success(pluginConfig, context) {
|
|
11
|
-
const { logger, nextRelease, linear,
|
|
44
|
+
const { logger, nextRelease, linear, commits } = context;
|
|
12
45
|
if (!linear) {
|
|
13
46
|
logger.log("Linear context not found, skipping issue updates");
|
|
14
47
|
return;
|
|
15
48
|
}
|
|
16
|
-
if (!
|
|
17
|
-
logger.log("No
|
|
49
|
+
if (!commits || commits.length === 0) {
|
|
50
|
+
logger.log("No commits found in release, skipping Linear updates");
|
|
18
51
|
return;
|
|
19
52
|
}
|
|
20
|
-
const { removeOldLabels = true, addComment = false, dryRun = false,
|
|
21
|
-
//
|
|
22
|
-
|
|
23
|
-
|
|
53
|
+
const { removeOldLabels = true, addComment = false, dryRun = false, } = pluginConfig;
|
|
54
|
+
// Find all branches that contributed to this release
|
|
55
|
+
const sourceBranches = await findSourceBranches(commits, logger);
|
|
56
|
+
if (sourceBranches.size === 0) {
|
|
57
|
+
logger.log("No source branches found, skipping Linear updates");
|
|
24
58
|
return;
|
|
25
59
|
}
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const issueIds = (0, parse_issues_1.parseIssuesFromBranch)(branch.name, linear.teamKeys);
|
|
35
|
-
if (issueIds.length === 0) {
|
|
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
|
-
}
|
|
60
|
+
// Extract Linear issue IDs from all found branches
|
|
61
|
+
const issueIds = new Set();
|
|
62
|
+
for (const branchName of Array.from(sourceBranches)) {
|
|
63
|
+
const branchIssues = (0, parse_issues_1.parseIssuesFromBranch)(branchName, linear.teamKeys);
|
|
64
|
+
branchIssues.forEach((id) => issueIds.add(id));
|
|
65
|
+
}
|
|
66
|
+
if (issueIds.size === 0) {
|
|
67
|
+
logger.log(`No Linear issues found in branches: ${Array.from(sourceBranches).join(", ")}`);
|
|
40
68
|
return;
|
|
41
69
|
}
|
|
42
|
-
logger.log(`Found ${issueIds.
|
|
70
|
+
logger.log(`Found ${issueIds.size} Linear issue(s): ${Array.from(issueIds).join(", ")} ` +
|
|
71
|
+
`from ${sourceBranches.size} branch(es)`);
|
|
43
72
|
if (dryRun) {
|
|
44
|
-
logger.log("[Dry run] Would update issues:", issueIds);
|
|
45
|
-
logger.log(`[Dry run] Would apply label: ${
|
|
46
|
-
logger.log(`[Dry run] Branch: ${branch.name}`);
|
|
73
|
+
logger.log("[Dry run] Would update issues:", Array.from(issueIds));
|
|
74
|
+
logger.log(`[Dry run] Would apply label: ${linear.labelPrefix}${nextRelease.version}`);
|
|
47
75
|
return;
|
|
48
76
|
}
|
|
77
|
+
// Initialize Linear client and prepare label
|
|
78
|
+
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
|
+
const labelColor = getLabelColor(nextRelease.type);
|
|
49
83
|
// Ensure the version label exists
|
|
50
84
|
const label = await client.ensureLabel(labelName, labelColor);
|
|
51
85
|
logger.log(`✓ Ensured label exists: ${labelName}`);
|
|
52
|
-
// Update each issue
|
|
53
|
-
const results = await Promise.allSettled(issueIds.map(async (issueId) => {
|
|
86
|
+
// Update each Linear issue
|
|
87
|
+
const results = await Promise.allSettled(Array.from(issueIds).map(async (issueId) => {
|
|
54
88
|
try {
|
|
55
|
-
// Get the issue first
|
|
56
89
|
const issue = await client.getIssue(issueId);
|
|
57
90
|
if (!issue) {
|
|
58
|
-
logger.warn(`Issue ${issueId} not found in Linear
|
|
91
|
+
logger.warn(`Issue ${issueId} not found in Linear`);
|
|
59
92
|
return { issueId, status: "not_found" };
|
|
60
93
|
}
|
|
61
94
|
// Remove old version labels if configured
|
|
@@ -66,7 +99,9 @@ async function success(pluginConfig, context) {
|
|
|
66
99
|
await client.addLabelToIssue(issue.id, label.id);
|
|
67
100
|
// Add comment if configured
|
|
68
101
|
if (addComment) {
|
|
69
|
-
const
|
|
102
|
+
const emoji = channel === "latest" ? "🚀" : "🔬";
|
|
103
|
+
const channelText = channel === "latest" ? "" : ` (${channel} channel)`;
|
|
104
|
+
const comment = `${emoji} Released in version ${version}${channelText}`;
|
|
70
105
|
await client.addComment(issue.id, comment);
|
|
71
106
|
}
|
|
72
107
|
logger.log(`✓ Updated issue ${issueId}`);
|
|
@@ -79,10 +114,9 @@ async function success(pluginConfig, context) {
|
|
|
79
114
|
}
|
|
80
115
|
}));
|
|
81
116
|
// Log summary
|
|
82
|
-
const updated = results.filter((r) => r.status === "fulfilled" && r.value
|
|
83
|
-
const failed = results.filter((r) => r.status === "rejected" ||
|
|
84
|
-
|
|
85
|
-
const notFound = results.filter((r) => r.status === "fulfilled" && r.value.status === "not_found").length;
|
|
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;
|
|
86
120
|
logger.log(`Linear update complete: ${updated} updated, ${failed} failed, ${notFound} not found`);
|
|
87
121
|
}
|
|
88
122
|
/**
|
|
@@ -100,17 +134,3 @@ function getLabelColor(releaseType) {
|
|
|
100
134
|
};
|
|
101
135
|
return colors[releaseType] || "#4752C4"; // Default blue
|
|
102
136
|
}
|
|
103
|
-
/**
|
|
104
|
-
* Format comment for Linear issue
|
|
105
|
-
*/
|
|
106
|
-
function formatComment(version, channel, release, branchName) {
|
|
107
|
-
const emoji = channel === "latest" ? "🚀" : "🔬";
|
|
108
|
-
const channelText = channel === "latest" ? "stable" : channel;
|
|
109
|
-
let comment = `${emoji} **Released in \`v${version}\`** (${channelText})\n\n`;
|
|
110
|
-
comment += `📌 Released from branch: \`${branchName}\`\n\n`;
|
|
111
|
-
const githubRepo = process.env.GITHUB_REPOSITORY;
|
|
112
|
-
if (release.gitTag && githubRepo) {
|
|
113
|
-
comment += `[View release →](https://github.com/${githubRepo}/releases/tag/${release.gitTag})`;
|
|
114
|
-
}
|
|
115
|
-
return comment;
|
|
116
|
-
}
|
package/dist/lib/verify.js
CHANGED
|
@@ -18,8 +18,12 @@ async function verifyConditions(pluginConfig, context) {
|
|
|
18
18
|
throw new error_1.default("No Linear API key found", "ENOLINEARTOKEN", "Please provide a Linear API key via plugin config or LINEAR_API_KEY environment variable.");
|
|
19
19
|
}
|
|
20
20
|
// Validate team keys format if provided
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
const teamKeyPattern = /^[A-Z]+$/;
|
|
22
|
+
const branchPattern = /^[A-Za-z0-9._-]+\/[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
23
|
+
const invalidTeamKeys = teamKeys.filter((key) => !teamKeyPattern.test(key) && !branchPattern.test(key));
|
|
24
|
+
if (invalidTeamKeys.length > 0) {
|
|
25
|
+
throw new error_1.default("Invalid team key format", "EINVALIDTEAMKEY", "Team keys must be uppercase letters (e.g. SD) or branch names (e.g. caio/tk-519-title). " +
|
|
26
|
+
`Invalid: ${invalidTeamKeys.join(", ")}`);
|
|
23
27
|
}
|
|
24
28
|
// Test API connection
|
|
25
29
|
const client = new linear_client_1.LinearClient(linearApiKey);
|
package/dist/lib/verify.test.js
CHANGED
|
@@ -11,6 +11,11 @@ jest.mock("@semantic-release/error", () => {
|
|
|
11
11
|
}
|
|
12
12
|
return { __esModule: true, default: SemanticReleaseError };
|
|
13
13
|
});
|
|
14
|
+
jest.mock("./linear-client", () => ({
|
|
15
|
+
LinearClient: jest.fn().mockImplementation(() => ({
|
|
16
|
+
testConnection: jest.fn().mockResolvedValue({}),
|
|
17
|
+
})),
|
|
18
|
+
}));
|
|
14
19
|
jest.mock("node-fetch", () => ({ __esModule: true, default: jest.fn() }));
|
|
15
20
|
const verify_1 = require("./verify");
|
|
16
21
|
describe("verify", () => {
|
|
@@ -27,4 +32,11 @@ describe("verify", () => {
|
|
|
27
32
|
// Validation happens BEFORE the API call, so it fails fast
|
|
28
33
|
await expect((0, verify_1.verifyConditions)({ apiKey: "test", teamKeys: ["eng-123"] }, mockContext)).rejects.toThrow("Invalid team key format");
|
|
29
34
|
});
|
|
35
|
+
test("accepts valid branch like team key", async () => {
|
|
36
|
+
// Accepts branch patterns without hitting API
|
|
37
|
+
await expect((0, verify_1.verifyConditions)({
|
|
38
|
+
apiKey: "test",
|
|
39
|
+
teamKeys: ["caio/tk-519-title"],
|
|
40
|
+
}, mockContext)).resolves.toBeUndefined();
|
|
41
|
+
});
|
|
30
42
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "semantic-release-linear-app",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0-next.2",
|
|
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,6 +46,7 @@
|
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"@semantic-release/error": "^4.0.0",
|
|
49
|
+
"execa": "^9.6.0",
|
|
49
50
|
"node-fetch": "^3.3.2"
|
|
50
51
|
},
|
|
51
52
|
"devDependencies": {
|
package/src/lib/success.ts
CHANGED
|
@@ -1,38 +1,70 @@
|
|
|
1
1
|
import { SuccessContext } from "semantic-release";
|
|
2
|
+
import { execa } from "execa";
|
|
2
3
|
import { LinearClient } from "./linear-client";
|
|
3
|
-
import { parseIssuesFromBranch
|
|
4
|
-
import {
|
|
5
|
-
PluginConfig,
|
|
6
|
-
LinearContext,
|
|
7
|
-
IssueUpdateResult,
|
|
8
|
-
ReleaseType,
|
|
9
|
-
} from "../types";
|
|
4
|
+
import { parseIssuesFromBranch } from "./parse-issues";
|
|
5
|
+
import { PluginConfig, LinearContext, ReleaseType } from "../types";
|
|
10
6
|
|
|
11
7
|
interface ExtendedContext extends SuccessContext {
|
|
12
8
|
linear?: LinearContext;
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Find source branches that contain the given commits
|
|
13
|
+
*/
|
|
14
|
+
async function findSourceBranches(
|
|
15
|
+
commits: readonly { hash: string }[],
|
|
16
|
+
logger: SuccessContext["logger"],
|
|
17
|
+
): Promise<Set<string>> {
|
|
18
|
+
const branches = new Set<string>();
|
|
19
|
+
|
|
20
|
+
if (commits.length === 0) return branches;
|
|
21
|
+
|
|
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]}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
} catch (error) {
|
|
46
|
+
logger.debug(`Git branch lookup failed: ${(error as Error).message}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return branches;
|
|
17
50
|
}
|
|
18
51
|
|
|
19
52
|
/**
|
|
20
53
|
* Update Linear issues after a successful release
|
|
21
|
-
* Only uses branch name for issue detection - single source of truth
|
|
22
54
|
*/
|
|
23
55
|
export async function success(
|
|
24
56
|
pluginConfig: PluginConfig,
|
|
25
57
|
context: ExtendedContext,
|
|
26
58
|
): Promise<void> {
|
|
27
|
-
const { logger, nextRelease, linear,
|
|
59
|
+
const { logger, nextRelease, linear, commits } = context;
|
|
28
60
|
|
|
29
61
|
if (!linear) {
|
|
30
62
|
logger.log("Linear context not found, skipping issue updates");
|
|
31
63
|
return;
|
|
32
64
|
}
|
|
33
65
|
|
|
34
|
-
if (!
|
|
35
|
-
logger.log("No
|
|
66
|
+
if (!commits || commits.length === 0) {
|
|
67
|
+
logger.log("No commits found in release, skipping Linear updates");
|
|
36
68
|
return;
|
|
37
69
|
}
|
|
38
70
|
|
|
@@ -40,67 +72,61 @@ export async function success(
|
|
|
40
72
|
removeOldLabels = true,
|
|
41
73
|
addComment = false,
|
|
42
74
|
dryRun = false,
|
|
43
|
-
skipBranches = ["main", "master", "develop", "staging", "production"],
|
|
44
|
-
requireIssueInBranch = true,
|
|
45
75
|
} = pluginConfig;
|
|
46
76
|
|
|
47
|
-
//
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
);
|
|
77
|
+
// Find all branches that contributed to this release
|
|
78
|
+
const sourceBranches = await findSourceBranches(commits, logger);
|
|
79
|
+
|
|
80
|
+
if (sourceBranches.size === 0) {
|
|
81
|
+
logger.log("No source branches found, skipping Linear updates");
|
|
52
82
|
return;
|
|
53
83
|
}
|
|
54
84
|
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const labelColor = getLabelColor(nextRelease.type as ReleaseType);
|
|
62
|
-
|
|
63
|
-
logger.log(
|
|
64
|
-
`Updating Linear issues for release ${version} (${channel}) from branch "${branch.name}"`,
|
|
65
|
-
);
|
|
66
|
-
|
|
67
|
-
// Extract issue IDs from branch name ONLY
|
|
68
|
-
const issueIds = parseIssuesFromBranch(branch.name, linear.teamKeys);
|
|
85
|
+
// Extract Linear issue IDs from all found branches
|
|
86
|
+
const issueIds = new Set<string>();
|
|
87
|
+
for (const branchName of Array.from(sourceBranches)) {
|
|
88
|
+
const branchIssues = parseIssuesFromBranch(branchName, linear.teamKeys);
|
|
89
|
+
branchIssues.forEach((id) => issueIds.add(id));
|
|
90
|
+
}
|
|
69
91
|
|
|
70
|
-
if (issueIds.
|
|
71
|
-
logger.log(
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
"⚠️ Consider using branch names like: feature/ENG-123-description",
|
|
75
|
-
);
|
|
76
|
-
}
|
|
92
|
+
if (issueIds.size === 0) {
|
|
93
|
+
logger.log(
|
|
94
|
+
`No Linear issues found in branches: ${Array.from(sourceBranches).join(", ")}`,
|
|
95
|
+
);
|
|
77
96
|
return;
|
|
78
97
|
}
|
|
79
98
|
|
|
80
99
|
logger.log(
|
|
81
|
-
`Found ${issueIds.
|
|
100
|
+
`Found ${issueIds.size} Linear issue(s): ${Array.from(issueIds).join(", ")} ` +
|
|
101
|
+
`from ${sourceBranches.size} branch(es)`,
|
|
82
102
|
);
|
|
83
103
|
|
|
84
104
|
if (dryRun) {
|
|
85
|
-
logger.log("[Dry run] Would update issues:", issueIds);
|
|
86
|
-
logger.log(
|
|
87
|
-
|
|
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
|
+
);
|
|
88
109
|
return;
|
|
89
110
|
}
|
|
90
111
|
|
|
112
|
+
// Initialize Linear client and prepare label
|
|
113
|
+
const client = new LinearClient(linear.apiKey);
|
|
114
|
+
const version = nextRelease.version;
|
|
115
|
+
const channel = nextRelease.channel || "latest";
|
|
116
|
+
const labelName = `${linear.labelPrefix}${version}`;
|
|
117
|
+
const labelColor = getLabelColor(nextRelease.type as ReleaseType);
|
|
118
|
+
|
|
91
119
|
// Ensure the version label exists
|
|
92
120
|
const label = await client.ensureLabel(labelName, labelColor);
|
|
93
121
|
logger.log(`✓ Ensured label exists: ${labelName}`);
|
|
94
122
|
|
|
95
|
-
// Update each issue
|
|
123
|
+
// Update each Linear issue
|
|
96
124
|
const results = await Promise.allSettled(
|
|
97
|
-
issueIds.map(async (issueId)
|
|
125
|
+
Array.from(issueIds).map(async (issueId) => {
|
|
98
126
|
try {
|
|
99
|
-
// Get the issue first
|
|
100
127
|
const issue = await client.getIssue(issueId);
|
|
101
|
-
|
|
102
128
|
if (!issue) {
|
|
103
|
-
logger.warn(`Issue ${issueId} not found in Linear
|
|
129
|
+
logger.warn(`Issue ${issueId} not found in Linear`);
|
|
104
130
|
return { issueId, status: "not_found" };
|
|
105
131
|
}
|
|
106
132
|
|
|
@@ -114,12 +140,10 @@ export async function success(
|
|
|
114
140
|
|
|
115
141
|
// Add comment if configured
|
|
116
142
|
if (addComment) {
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
channel
|
|
120
|
-
|
|
121
|
-
branch.name,
|
|
122
|
-
);
|
|
143
|
+
const emoji = channel === "latest" ? "🚀" : "🔬";
|
|
144
|
+
const channelText =
|
|
145
|
+
channel === "latest" ? "" : ` (${channel} channel)`;
|
|
146
|
+
const comment = `${emoji} Released in version ${version}${channelText}`;
|
|
123
147
|
await client.addComment(issue.id, comment);
|
|
124
148
|
}
|
|
125
149
|
|
|
@@ -135,17 +159,13 @@ export async function success(
|
|
|
135
159
|
|
|
136
160
|
// Log summary
|
|
137
161
|
const updated = results.filter(
|
|
138
|
-
(r) => r.status === "fulfilled" && r.value
|
|
162
|
+
(r) => r.status === "fulfilled" && r.value?.status === "updated",
|
|
139
163
|
).length;
|
|
140
|
-
|
|
141
164
|
const failed = results.filter(
|
|
142
|
-
(r) =>
|
|
143
|
-
r.status === "rejected" ||
|
|
144
|
-
(r.status === "fulfilled" && r.value.status === "failed"),
|
|
165
|
+
(r) => r.status === "rejected" || r.value?.status === "failed",
|
|
145
166
|
).length;
|
|
146
|
-
|
|
147
167
|
const notFound = results.filter(
|
|
148
|
-
(r) => r.status === "fulfilled" && r.value
|
|
168
|
+
(r) => r.status === "fulfilled" && r.value?.status === "not_found",
|
|
149
169
|
).length;
|
|
150
170
|
|
|
151
171
|
logger.log(
|
|
@@ -169,26 +189,3 @@ function getLabelColor(releaseType: ReleaseType): string {
|
|
|
169
189
|
|
|
170
190
|
return colors[releaseType] || "#4752C4"; // Default blue
|
|
171
191
|
}
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* Format comment for Linear issue
|
|
175
|
-
*/
|
|
176
|
-
function formatComment(
|
|
177
|
-
version: string,
|
|
178
|
-
channel: string,
|
|
179
|
-
release: SuccessContext["nextRelease"],
|
|
180
|
-
branchName: string,
|
|
181
|
-
): string {
|
|
182
|
-
const emoji = channel === "latest" ? "🚀" : "🔬";
|
|
183
|
-
const channelText = channel === "latest" ? "stable" : channel;
|
|
184
|
-
|
|
185
|
-
let comment = `${emoji} **Released in \`v${version}\`** (${channelText})\n\n`;
|
|
186
|
-
comment += `📌 Released from branch: \`${branchName}\`\n\n`;
|
|
187
|
-
|
|
188
|
-
const githubRepo = process.env.GITHUB_REPOSITORY;
|
|
189
|
-
if (release.gitTag && githubRepo) {
|
|
190
|
-
comment += `[View release →](https://github.com/${githubRepo}/releases/tag/${release.gitTag})`;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
return comment;
|
|
194
|
-
}
|
package/src/lib/verify.test.ts
CHANGED
|
@@ -10,6 +10,12 @@ jest.mock("@semantic-release/error", () => {
|
|
|
10
10
|
return { __esModule: true, default: SemanticReleaseError };
|
|
11
11
|
});
|
|
12
12
|
|
|
13
|
+
jest.mock("./linear-client", () => ({
|
|
14
|
+
LinearClient: jest.fn().mockImplementation(() => ({
|
|
15
|
+
testConnection: jest.fn().mockResolvedValue({}),
|
|
16
|
+
})),
|
|
17
|
+
}));
|
|
18
|
+
|
|
13
19
|
jest.mock("node-fetch", () => ({ __esModule: true, default: jest.fn() }));
|
|
14
20
|
|
|
15
21
|
import { verifyConditions } from "./verify";
|
|
@@ -36,4 +42,17 @@ describe("verify", () => {
|
|
|
36
42
|
verifyConditions({ apiKey: "test", teamKeys: ["eng-123"] }, mockContext),
|
|
37
43
|
).rejects.toThrow("Invalid team key format");
|
|
38
44
|
});
|
|
45
|
+
|
|
46
|
+
test("accepts valid branch like team key", async () => {
|
|
47
|
+
// Accepts branch patterns without hitting API
|
|
48
|
+
await expect(
|
|
49
|
+
verifyConditions(
|
|
50
|
+
{
|
|
51
|
+
apiKey: "test",
|
|
52
|
+
teamKeys: ["caio/tk-519-title"],
|
|
53
|
+
},
|
|
54
|
+
mockContext,
|
|
55
|
+
),
|
|
56
|
+
).resolves.toBeUndefined();
|
|
57
|
+
});
|
|
39
58
|
});
|
package/src/lib/verify.ts
CHANGED
|
@@ -25,11 +25,18 @@ export async function verifyConditions(
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
// Validate team keys format if provided
|
|
28
|
-
|
|
28
|
+
const teamKeyPattern = /^[A-Z]+$/;
|
|
29
|
+
const branchPattern = /^[A-Za-z0-9._-]+\/[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
30
|
+
const invalidTeamKeys = teamKeys.filter(
|
|
31
|
+
(key) => !teamKeyPattern.test(key) && !branchPattern.test(key),
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
if (invalidTeamKeys.length > 0) {
|
|
29
35
|
throw new SemanticReleaseError(
|
|
30
36
|
"Invalid team key format",
|
|
31
37
|
"EINVALIDTEAMKEY",
|
|
32
|
-
|
|
38
|
+
"Team keys must be uppercase letters (e.g. SD) or branch names (e.g. caio/tk-519-title). " +
|
|
39
|
+
`Invalid: ${invalidTeamKeys.join(", ")}`,
|
|
33
40
|
);
|
|
34
41
|
}
|
|
35
42
|
|