deploy-vir 3.5.0 → 3.6.1
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/commit.d.ts +24 -5
- package/dist/commit.js +61 -43
- package/dist/git.js +8 -2
- package/dist/notifications/notification-params.d.ts +1 -1
- package/dist/notifications/slack.js +19 -4
- package/package.json +24 -22
- package/src/commit.ts +98 -56
- package/src/git.ts +8 -2
- package/src/notifications/notification-params.ts +1 -1
- package/src/notifications/slack.ts +27 -6
package/dist/commit.d.ts
CHANGED
|
@@ -7,11 +7,30 @@ import { type DefaultLogFields, type ListLogLine } from 'simple-git';
|
|
|
7
7
|
*/
|
|
8
8
|
export type Commit = DefaultLogFields & ListLogLine;
|
|
9
9
|
/**
|
|
10
|
-
* Extract the
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* Extract the owning person's name for a commit: the first assignee on the commit's pull request,
|
|
11
|
+
* or that pull request's author (requires the `gh` CLI). Whoever merged a pull request is often not
|
|
12
|
+
* whoever owns the change, so the commit's own author is only used when no pull request can be
|
|
13
|
+
* read.
|
|
14
14
|
*
|
|
15
15
|
* @category Internal
|
|
16
16
|
*/
|
|
17
|
-
export declare function getCommitAuthorName(commit
|
|
17
|
+
export declare function getCommitAuthorName({ commit, gitUrl, }: Readonly<{
|
|
18
|
+
commit: Readonly<Pick<Commit, 'author_name'> & PartialWithUndefined<Pick<Commit, 'hash' | 'message'>>>;
|
|
19
|
+
gitUrl: string;
|
|
20
|
+
}>): Promise<string>;
|
|
21
|
+
/**
|
|
22
|
+
* Build the `gh` command that reads a pull request's assignees and author.
|
|
23
|
+
*
|
|
24
|
+
* `gh` picks its repo from the current directory's git remotes, which is not necessarily the repo
|
|
25
|
+
* being deployed, so the repo is always pinned with `--repo`. Without that, a pull request number
|
|
26
|
+
* from the deployed repo silently resolves to whatever unrelated pull request shares that number in
|
|
27
|
+
* the current directory's repo.
|
|
28
|
+
*
|
|
29
|
+
* @category Internal
|
|
30
|
+
*/
|
|
31
|
+
export declare function buildPullRequestAuthorCommand({ pullRequestNumber, hash, gitUrl, }: Readonly<{
|
|
32
|
+
gitUrl: string;
|
|
33
|
+
} & PartialWithUndefined<{
|
|
34
|
+
pullRequestNumber: string;
|
|
35
|
+
hash: string;
|
|
36
|
+
}>>): string;
|
package/dist/commit.js
CHANGED
|
@@ -1,75 +1,93 @@
|
|
|
1
1
|
import { check } from '@augment-vir/assert';
|
|
2
2
|
import { extractErrorMessage, log } from '@augment-vir/common';
|
|
3
3
|
import { runShellCommand } from '@augment-vir/node';
|
|
4
|
-
const coAuthorRegExp = /^co[- \t]*authored[- \t]*by:(.*)$/gim;
|
|
5
4
|
const pullRequestNumberRegExp = /#(\d+)/;
|
|
6
|
-
const botNameSuffix = '[bot]';
|
|
7
5
|
/**
|
|
8
6
|
* Each `gh` lookup is a network request and each commit's author name gets read more than once (CLI
|
|
9
7
|
* output and notifications), so results are cached by their lookup key.
|
|
10
8
|
*/
|
|
11
|
-
const
|
|
9
|
+
const pullRequestAuthorCache = new Map();
|
|
12
10
|
/**
|
|
13
|
-
* Extract the
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
11
|
+
* Extract the owning person's name for a commit: the first assignee on the commit's pull request,
|
|
12
|
+
* or that pull request's author (requires the `gh` CLI). Whoever merged a pull request is often not
|
|
13
|
+
* whoever owns the change, so the commit's own author is only used when no pull request can be
|
|
14
|
+
* read.
|
|
17
15
|
*
|
|
18
16
|
* @category Internal
|
|
19
17
|
*/
|
|
20
|
-
export async function getCommitAuthorName(commit) {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
.find((name) => name && !name.endsWith(botNameSuffix));
|
|
28
|
-
return coAuthorName || (await getPullRequestAssigneeName(commit)) || commit.author_name;
|
|
18
|
+
export async function getCommitAuthorName({ commit, gitUrl, }) {
|
|
19
|
+
const pullRequestAuthorName = await getPullRequestAuthorName({
|
|
20
|
+
pullRequestNumber: commit.message?.match(pullRequestNumberRegExp)?.[1],
|
|
21
|
+
hash: commit.hash,
|
|
22
|
+
gitUrl,
|
|
23
|
+
});
|
|
24
|
+
return pullRequestAuthorName || commit.author_name;
|
|
29
25
|
}
|
|
30
|
-
function
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
if (!cacheKey) {
|
|
26
|
+
function getPullRequestAuthorName({ pullRequestNumber, hash, gitUrl, }) {
|
|
27
|
+
const pullRequestKey = pullRequestNumber || hash;
|
|
28
|
+
if (!pullRequestKey) {
|
|
34
29
|
return Promise.resolve(undefined);
|
|
35
30
|
}
|
|
36
|
-
|
|
31
|
+
/** Each repo gets its own cache entry because pull request numbers repeat across repos. */
|
|
32
|
+
const cacheKey = [
|
|
33
|
+
gitUrl,
|
|
34
|
+
pullRequestKey,
|
|
35
|
+
].join(' ');
|
|
36
|
+
const cached = pullRequestAuthorCache.get(cacheKey);
|
|
37
37
|
if (cached) {
|
|
38
38
|
return cached;
|
|
39
39
|
}
|
|
40
|
-
const pending =
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
const pending = fetchPullRequestAuthorName(buildPullRequestAuthorCommand({
|
|
41
|
+
gitUrl,
|
|
42
|
+
hash,
|
|
43
|
+
pullRequestNumber,
|
|
44
|
+
}));
|
|
45
|
+
pullRequestAuthorCache.set(cacheKey, pending);
|
|
44
46
|
return pending;
|
|
45
47
|
}
|
|
46
|
-
|
|
48
|
+
/**
|
|
49
|
+
* Build the `gh` command that reads a pull request's assignees and author.
|
|
50
|
+
*
|
|
51
|
+
* `gh` picks its repo from the current directory's git remotes, which is not necessarily the repo
|
|
52
|
+
* being deployed, so the repo is always pinned with `--repo`. Without that, a pull request number
|
|
53
|
+
* from the deployed repo silently resolves to whatever unrelated pull request shares that number in
|
|
54
|
+
* the current directory's repo.
|
|
55
|
+
*
|
|
56
|
+
* @category Internal
|
|
57
|
+
*/
|
|
58
|
+
export function buildPullRequestAuthorCommand({ pullRequestNumber, hash, gitUrl, }) {
|
|
59
|
+
const repoFlag = `--repo '${gitUrl}'`;
|
|
60
|
+
return pullRequestNumber
|
|
61
|
+
? `gh pr view ${pullRequestNumber} ${repoFlag} --json assignees,author`
|
|
62
|
+
: `gh pr list --search ${hash} ${repoFlag} --state merged --limit 1 --json assignees,author`;
|
|
63
|
+
}
|
|
64
|
+
async function fetchPullRequestAuthorName(command) {
|
|
47
65
|
try {
|
|
48
66
|
const output = await runShellCommand(command, {
|
|
49
67
|
rejectOnError: true,
|
|
50
68
|
});
|
|
51
|
-
return
|
|
69
|
+
return extractPullRequestAuthorName(JSON.parse(output.stdout));
|
|
52
70
|
}
|
|
53
71
|
catch (error) {
|
|
54
|
-
log.faint(`Failed to read a pull request
|
|
72
|
+
log.faint(`Failed to read a pull request author: ${extractErrorMessage(error)}`);
|
|
55
73
|
return undefined;
|
|
56
74
|
}
|
|
57
75
|
}
|
|
58
|
-
function
|
|
76
|
+
function extractPullRequestAuthorName(parsedJson) {
|
|
59
77
|
/** `gh pr list` outputs an array, `gh pr view` outputs a single pull request. */
|
|
60
78
|
const pullRequest = check.isArray(parsedJson) ? parsedJson[0] : parsedJson;
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
return
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
79
|
+
const assignees = check.hasKey(pullRequest, 'assignees') && check.isArray(pullRequest.assignees)
|
|
80
|
+
? pullRequest.assignees
|
|
81
|
+
: [];
|
|
82
|
+
return [
|
|
83
|
+
...assignees,
|
|
84
|
+
check.hasKey(pullRequest, 'author') ? pullRequest.author : undefined,
|
|
85
|
+
]
|
|
86
|
+
.map(extractUserName)
|
|
87
|
+
.find((name) => name);
|
|
88
|
+
}
|
|
89
|
+
function extractUserName(user) {
|
|
90
|
+
const name = check.hasKey(user, 'name') && check.isString(user.name) ? user.name : '';
|
|
91
|
+
const login = check.hasKey(user, 'login') && check.isString(user.login) ? user.login : '';
|
|
92
|
+
return name || login || undefined;
|
|
75
93
|
}
|
package/dist/git.js
CHANGED
|
@@ -68,12 +68,18 @@ export async function pushDeploy(git, branchConfig, repoConfig, remoteName, noti
|
|
|
68
68
|
const requiresForcePush = commitsBehind.length > 0;
|
|
69
69
|
log.info(`\n${requiresForcePush ? 'Only on' : 'Releasing from'} ${logColors.bold}${fromBranch}${logColors.reset}:`);
|
|
70
70
|
await awaitedForEach(commitsAhead, async (commit, index) => {
|
|
71
|
-
log.faint(` ${index + 1}. ${commit.hash.slice(0, 7)} (${await getCommitAuthorName(
|
|
71
|
+
log.faint(` ${index + 1}. ${commit.hash.slice(0, 7)} (${await getCommitAuthorName({
|
|
72
|
+
commit,
|
|
73
|
+
gitUrl: repoConfig.gitUrl,
|
|
74
|
+
})}) - ${commit.message}`);
|
|
72
75
|
});
|
|
73
76
|
if (requiresForcePush) {
|
|
74
77
|
log.info(`\nOnly on ${logColors.bold}${toBranch}${logColors.reset}:`);
|
|
75
78
|
await awaitedForEach(commitsBehind, async (commit, index) => {
|
|
76
|
-
log.faint(` ${index + 1}. ${commit.hash.slice(0, 7)} (${await getCommitAuthorName(
|
|
79
|
+
log.faint(` ${index + 1}. ${commit.hash.slice(0, 7)} (${await getCommitAuthorName({
|
|
80
|
+
commit,
|
|
81
|
+
gitUrl: repoConfig.gitUrl,
|
|
82
|
+
})}) - ${commit.message}`);
|
|
77
83
|
});
|
|
78
84
|
}
|
|
79
85
|
const shouldPush = bypassConfirmation ||
|
|
@@ -8,7 +8,7 @@ import { type DeployResult } from '../git.js';
|
|
|
8
8
|
export type NotificationParams<Target extends NotificationTarget = NotificationTarget> = {
|
|
9
9
|
deployResult: Readonly<DeployResult>;
|
|
10
10
|
branchConfig: Readonly<DeployVirBranchConfig>;
|
|
11
|
-
repoConfig: Readonly<Pick<DeployVirRepoConfig, 'commitBaseUrl' | 'name'>>;
|
|
11
|
+
repoConfig: Readonly<Pick<DeployVirRepoConfig, 'commitBaseUrl' | 'gitUrl' | 'name'>>;
|
|
12
12
|
notification: Readonly<Extract<NotificationConfig, {
|
|
13
13
|
target: Target;
|
|
14
14
|
}>>;
|
|
@@ -9,8 +9,11 @@ function truncateString(value, truncateAt = 100) {
|
|
|
9
9
|
return value;
|
|
10
10
|
}
|
|
11
11
|
}
|
|
12
|
-
async function formatCommit(baseCommitUrl, commit) {
|
|
13
|
-
const authorName = await getCommitAuthorName(
|
|
12
|
+
async function formatCommit({ baseCommitUrl, commit, gitUrl, }) {
|
|
13
|
+
const authorName = await getCommitAuthorName({
|
|
14
|
+
commit,
|
|
15
|
+
gitUrl,
|
|
16
|
+
});
|
|
14
17
|
if (baseCommitUrl) {
|
|
15
18
|
return `<${joinUrlPaths(baseCommitUrl, commit.hash)}|${commit.hash.slice(0, 7)}> (${authorName}) ${truncateString(commit.message)}`;
|
|
16
19
|
}
|
|
@@ -44,8 +47,20 @@ function chunkSectionLines(lines) {
|
|
|
44
47
|
* @category Internal
|
|
45
48
|
*/
|
|
46
49
|
export async function sendNotificationToSlack({ deployResult: { deployCommits: { deployedCommits, overwrittenCommits }, branchStatus: { after, before }, toBranchName, }, hookResult, notification, repoConfig, }) {
|
|
47
|
-
const deployedCommitBullets = await Promise.all(deployedCommits.map(async (commit) =>
|
|
48
|
-
|
|
50
|
+
const deployedCommitBullets = await Promise.all(deployedCommits.map(async (commit) => {
|
|
51
|
+
return await formatCommit({
|
|
52
|
+
baseCommitUrl: repoConfig.commitBaseUrl,
|
|
53
|
+
commit,
|
|
54
|
+
gitUrl: repoConfig.gitUrl,
|
|
55
|
+
});
|
|
56
|
+
}));
|
|
57
|
+
const overwrittenCommitBullets = await Promise.all(overwrittenCommits.map(async (commit) => {
|
|
58
|
+
return await formatCommit({
|
|
59
|
+
baseCommitUrl: repoConfig.commitBaseUrl,
|
|
60
|
+
commit,
|
|
61
|
+
gitUrl: repoConfig.gitUrl,
|
|
62
|
+
});
|
|
63
|
+
}));
|
|
49
64
|
const deployedCommitBulletLines = deployedCommitBullets.map((bullet) => `- ${bullet}`);
|
|
50
65
|
const overwrittenCommitBulletLines = overwrittenCommitBullets.length
|
|
51
66
|
? overwrittenCommitBullets.map((bullet) => `- ${bullet}`)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "deploy-vir",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.6.1",
|
|
4
4
|
"description": "Deploy a branch to another branch with notifications.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"deploy",
|
|
@@ -25,7 +25,9 @@
|
|
|
25
25
|
"main": "dist/index.js",
|
|
26
26
|
"module": "dist/index.js",
|
|
27
27
|
"types": "dist/index.d.ts",
|
|
28
|
-
"bin":
|
|
28
|
+
"bin": {
|
|
29
|
+
"deploy-vir": "bin.js"
|
|
30
|
+
},
|
|
29
31
|
"scripts": {
|
|
30
32
|
"compile": "virmator compile",
|
|
31
33
|
"docs": "virmator docs",
|
|
@@ -42,39 +44,39 @@
|
|
|
42
44
|
"test:update": "npm run test update"
|
|
43
45
|
},
|
|
44
46
|
"dependencies": {
|
|
45
|
-
"@augment-vir/assert": "^32.
|
|
46
|
-
"@augment-vir/common": "^32.
|
|
47
|
-
"@augment-vir/node": "^32.
|
|
48
|
-
"@inquirer/prompts": "^8.
|
|
49
|
-
"@slack/web-api": "^8.
|
|
47
|
+
"@augment-vir/assert": "^32.3.0",
|
|
48
|
+
"@augment-vir/common": "^32.3.0",
|
|
49
|
+
"@augment-vir/node": "^32.3.0",
|
|
50
|
+
"@inquirer/prompts": "^8.7.2",
|
|
51
|
+
"@slack/web-api": "^8.1.1",
|
|
50
52
|
"cli-vir": "^1.0.5",
|
|
51
53
|
"simple-git": "^3.36.0",
|
|
52
|
-
"tsx": "^4.23.
|
|
53
|
-
"url-vir": "^2.2.
|
|
54
|
+
"tsx": "^4.23.13",
|
|
55
|
+
"url-vir": "^2.2.2"
|
|
54
56
|
},
|
|
55
57
|
"devDependencies": {
|
|
56
|
-
"@augment-vir/test": "^32.
|
|
57
|
-
"@eslint/eslintrc": "^3.3.
|
|
58
|
+
"@augment-vir/test": "^32.3.0",
|
|
59
|
+
"@eslint/eslintrc": "^3.3.7",
|
|
58
60
|
"@eslint/js": "^10.0.1",
|
|
59
61
|
"@stylistic/eslint-plugin": "^5.10.0",
|
|
60
62
|
"@stylistic/eslint-plugin-ts": "^4.4.1",
|
|
61
|
-
"@types/node": "^26.
|
|
62
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
63
|
-
"cspell": "^10.0
|
|
64
|
-
"dependency-cruiser": "^18.
|
|
63
|
+
"@types/node": "^26.5.0",
|
|
64
|
+
"@typescript-eslint/eslint-plugin": "^8.70.0",
|
|
65
|
+
"cspell": "^10.3.0",
|
|
66
|
+
"dependency-cruiser": "^18.2.0",
|
|
65
67
|
"esbuild": "^0.28.2",
|
|
66
|
-
"eslint": "^10.
|
|
68
|
+
"eslint": "^10.10.0",
|
|
67
69
|
"eslint-config-prettier": "^10.1.8",
|
|
68
|
-
"eslint-plugin-jsdoc": "^64.
|
|
70
|
+
"eslint-plugin-jsdoc": "^64.3.8",
|
|
69
71
|
"eslint-plugin-monorepo-cop": "^1.0.2",
|
|
70
72
|
"eslint-plugin-playwright": "^2.11.0",
|
|
71
73
|
"eslint-plugin-prettier": "^5.5.6",
|
|
72
74
|
"eslint-plugin-require-extensions": "^0.1.3",
|
|
73
75
|
"eslint-plugin-sonarjs": "^4.2.0",
|
|
74
|
-
"eslint-plugin-unicorn": "^
|
|
76
|
+
"eslint-plugin-unicorn": "^74.0.0",
|
|
75
77
|
"istanbul-smart-text-reporter": "^1.1.5",
|
|
76
78
|
"markdown-code-example-inserter": "^3.0.6",
|
|
77
|
-
"npm-check-updates": "^23.0
|
|
79
|
+
"npm-check-updates": "^23.1.0",
|
|
78
80
|
"prettier": "~3.3.3",
|
|
79
81
|
"prettier-plugin-interpolated-html-tags": "^2.0.2",
|
|
80
82
|
"prettier-plugin-jsdoc": "^1.8.1",
|
|
@@ -83,11 +85,11 @@
|
|
|
83
85
|
"prettier-plugin-packagejson": "^3.0.2",
|
|
84
86
|
"prettier-plugin-sort-json": "^4.2.0",
|
|
85
87
|
"prettier-plugin-toml": "^2.0.6",
|
|
86
|
-
"runstorm": "^1.0
|
|
88
|
+
"runstorm": "^1.1.0",
|
|
87
89
|
"typedoc": "^0.28.20",
|
|
88
90
|
"typescript": "^6.0.3",
|
|
89
|
-
"typescript-eslint": "^8.
|
|
90
|
-
"virmator": "^14.
|
|
91
|
+
"typescript-eslint": "^8.70.0",
|
|
92
|
+
"virmator": "^14.36.2"
|
|
91
93
|
},
|
|
92
94
|
"engines": {
|
|
93
95
|
"node": ">=22"
|
package/src/commit.ts
CHANGED
|
@@ -10,101 +10,143 @@ import {type DefaultLogFields, type ListLogLine} from 'simple-git';
|
|
|
10
10
|
*/
|
|
11
11
|
export type Commit = DefaultLogFields & ListLogLine;
|
|
12
12
|
|
|
13
|
-
const coAuthorRegExp = /^co[- \t]*authored[- \t]*by:(.*)$/gim;
|
|
14
13
|
const pullRequestNumberRegExp = /#(\d+)/;
|
|
15
|
-
const botNameSuffix = '[bot]';
|
|
16
14
|
|
|
17
15
|
/**
|
|
18
16
|
* Each `gh` lookup is a network request and each commit's author name gets read more than once (CLI
|
|
19
17
|
* output and notifications), so results are cached by their lookup key.
|
|
20
18
|
*/
|
|
21
|
-
const
|
|
19
|
+
const pullRequestAuthorCache = new Map<string, Promise<string | undefined>>();
|
|
22
20
|
|
|
23
21
|
/**
|
|
24
|
-
* Extract the
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
22
|
+
* Extract the owning person's name for a commit: the first assignee on the commit's pull request,
|
|
23
|
+
* or that pull request's author (requires the `gh` CLI). Whoever merged a pull request is often not
|
|
24
|
+
* whoever owns the change, so the commit's own author is only used when no pull request can be
|
|
25
|
+
* read.
|
|
28
26
|
*
|
|
29
27
|
* @category Internal
|
|
30
28
|
*/
|
|
31
|
-
export async function getCommitAuthorName(
|
|
29
|
+
export async function getCommitAuthorName({
|
|
30
|
+
commit,
|
|
31
|
+
gitUrl,
|
|
32
|
+
}: Readonly<{
|
|
32
33
|
commit: Readonly<
|
|
33
|
-
Pick<Commit, 'author_name' | '
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
): Promise<string> {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
.find((name) => name && !name.endsWith(botNameSuffix));
|
|
45
|
-
|
|
46
|
-
return coAuthorName || (await getPullRequestAssigneeName(commit)) || commit.author_name;
|
|
34
|
+
Pick<Commit, 'author_name'> & PartialWithUndefined<Pick<Commit, 'hash' | 'message'>>
|
|
35
|
+
>;
|
|
36
|
+
gitUrl: string;
|
|
37
|
+
}>): Promise<string> {
|
|
38
|
+
const pullRequestAuthorName = await getPullRequestAuthorName({
|
|
39
|
+
pullRequestNumber: commit.message?.match(pullRequestNumberRegExp)?.[1],
|
|
40
|
+
hash: commit.hash,
|
|
41
|
+
gitUrl,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return pullRequestAuthorName || commit.author_name;
|
|
47
45
|
}
|
|
48
46
|
|
|
49
|
-
function
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
47
|
+
function getPullRequestAuthorName({
|
|
48
|
+
pullRequestNumber,
|
|
49
|
+
hash,
|
|
50
|
+
gitUrl,
|
|
51
|
+
}: Readonly<
|
|
52
|
+
{
|
|
53
|
+
gitUrl: string;
|
|
54
|
+
} & PartialWithUndefined<{
|
|
55
|
+
pullRequestNumber: string;
|
|
56
|
+
hash: string;
|
|
57
|
+
}>
|
|
58
|
+
>): Promise<string | undefined> {
|
|
59
|
+
const pullRequestKey = pullRequestNumber || hash;
|
|
60
|
+
|
|
61
|
+
if (!pullRequestKey) {
|
|
56
62
|
return Promise.resolve(undefined);
|
|
57
63
|
}
|
|
58
64
|
|
|
59
|
-
|
|
65
|
+
/** Each repo gets its own cache entry because pull request numbers repeat across repos. */
|
|
66
|
+
const cacheKey = [
|
|
67
|
+
gitUrl,
|
|
68
|
+
pullRequestKey,
|
|
69
|
+
].join(' ');
|
|
70
|
+
|
|
71
|
+
const cached = pullRequestAuthorCache.get(cacheKey);
|
|
60
72
|
|
|
61
73
|
if (cached) {
|
|
62
74
|
return cached;
|
|
63
75
|
}
|
|
64
76
|
|
|
65
|
-
const pending =
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
77
|
+
const pending = fetchPullRequestAuthorName(
|
|
78
|
+
buildPullRequestAuthorCommand({
|
|
79
|
+
gitUrl,
|
|
80
|
+
hash,
|
|
81
|
+
pullRequestNumber,
|
|
82
|
+
}),
|
|
69
83
|
);
|
|
70
|
-
|
|
84
|
+
pullRequestAuthorCache.set(cacheKey, pending);
|
|
71
85
|
|
|
72
86
|
return pending;
|
|
73
87
|
}
|
|
74
88
|
|
|
75
|
-
|
|
89
|
+
/**
|
|
90
|
+
* Build the `gh` command that reads a pull request's assignees and author.
|
|
91
|
+
*
|
|
92
|
+
* `gh` picks its repo from the current directory's git remotes, which is not necessarily the repo
|
|
93
|
+
* being deployed, so the repo is always pinned with `--repo`. Without that, a pull request number
|
|
94
|
+
* from the deployed repo silently resolves to whatever unrelated pull request shares that number in
|
|
95
|
+
* the current directory's repo.
|
|
96
|
+
*
|
|
97
|
+
* @category Internal
|
|
98
|
+
*/
|
|
99
|
+
export function buildPullRequestAuthorCommand({
|
|
100
|
+
pullRequestNumber,
|
|
101
|
+
hash,
|
|
102
|
+
gitUrl,
|
|
103
|
+
}: Readonly<
|
|
104
|
+
{
|
|
105
|
+
gitUrl: string;
|
|
106
|
+
} & PartialWithUndefined<{
|
|
107
|
+
pullRequestNumber: string;
|
|
108
|
+
hash: string;
|
|
109
|
+
}>
|
|
110
|
+
>): string {
|
|
111
|
+
const repoFlag = `--repo '${gitUrl}'`;
|
|
112
|
+
|
|
113
|
+
return pullRequestNumber
|
|
114
|
+
? `gh pr view ${pullRequestNumber} ${repoFlag} --json assignees,author`
|
|
115
|
+
: `gh pr list --search ${hash} ${repoFlag} --state merged --limit 1 --json assignees,author`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function fetchPullRequestAuthorName(command: string): Promise<string | undefined> {
|
|
76
119
|
try {
|
|
77
120
|
const output = await runShellCommand(command, {
|
|
78
121
|
rejectOnError: true,
|
|
79
122
|
});
|
|
80
123
|
|
|
81
|
-
return
|
|
124
|
+
return extractPullRequestAuthorName(JSON.parse(output.stdout));
|
|
82
125
|
} catch (error) {
|
|
83
|
-
log.faint(`Failed to read a pull request
|
|
126
|
+
log.faint(`Failed to read a pull request author: ${extractErrorMessage(error)}`);
|
|
84
127
|
return undefined;
|
|
85
128
|
}
|
|
86
129
|
}
|
|
87
130
|
|
|
88
|
-
function
|
|
131
|
+
function extractPullRequestAuthorName(parsedJson: unknown): string | undefined {
|
|
89
132
|
/** `gh pr list` outputs an array, `gh pr view` outputs a single pull request. */
|
|
90
133
|
const pullRequest = check.isArray(parsedJson) ? parsedJson[0] : parsedJson;
|
|
134
|
+
const assignees =
|
|
135
|
+
check.hasKey(pullRequest, 'assignees') && check.isArray(pullRequest.assignees)
|
|
136
|
+
? pullRequest.assignees
|
|
137
|
+
: [];
|
|
138
|
+
|
|
139
|
+
return [
|
|
140
|
+
...assignees,
|
|
141
|
+
check.hasKey(pullRequest, 'author') ? pullRequest.author : undefined,
|
|
142
|
+
]
|
|
143
|
+
.map(extractUserName)
|
|
144
|
+
.find((name) => name);
|
|
145
|
+
}
|
|
91
146
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
147
|
+
function extractUserName(user: unknown): string | undefined {
|
|
148
|
+
const name = check.hasKey(user, 'name') && check.isString(user.name) ? user.name : '';
|
|
149
|
+
const login = check.hasKey(user, 'login') && check.isString(user.login) ? user.login : '';
|
|
95
150
|
|
|
96
|
-
return
|
|
97
|
-
.map((assignee) => {
|
|
98
|
-
const name =
|
|
99
|
-
check.hasKey(assignee, 'name') && check.isString(assignee.name)
|
|
100
|
-
? assignee.name
|
|
101
|
-
: '';
|
|
102
|
-
const login =
|
|
103
|
-
check.hasKey(assignee, 'login') && check.isString(assignee.login)
|
|
104
|
-
? assignee.login
|
|
105
|
-
: '';
|
|
106
|
-
|
|
107
|
-
return name || login;
|
|
108
|
-
})
|
|
109
|
-
.find((name) => name && !name.endsWith(botNameSuffix));
|
|
151
|
+
return name || login || undefined;
|
|
110
152
|
}
|
package/src/git.ts
CHANGED
|
@@ -158,7 +158,10 @@ export async function pushDeploy(
|
|
|
158
158
|
);
|
|
159
159
|
await awaitedForEach(commitsAhead, async (commit, index) => {
|
|
160
160
|
log.faint(
|
|
161
|
-
` ${index + 1}. ${commit.hash.slice(0, 7)} (${await getCommitAuthorName(
|
|
161
|
+
` ${index + 1}. ${commit.hash.slice(0, 7)} (${await getCommitAuthorName({
|
|
162
|
+
commit,
|
|
163
|
+
gitUrl: repoConfig.gitUrl,
|
|
164
|
+
})}) - ${commit.message}`,
|
|
162
165
|
);
|
|
163
166
|
});
|
|
164
167
|
|
|
@@ -166,7 +169,10 @@ export async function pushDeploy(
|
|
|
166
169
|
log.info(`\nOnly on ${logColors.bold}${toBranch}${logColors.reset}:`);
|
|
167
170
|
await awaitedForEach(commitsBehind, async (commit, index) => {
|
|
168
171
|
log.faint(
|
|
169
|
-
` ${index + 1}. ${commit.hash.slice(0, 7)} (${await getCommitAuthorName(
|
|
172
|
+
` ${index + 1}. ${commit.hash.slice(0, 7)} (${await getCommitAuthorName({
|
|
173
|
+
commit,
|
|
174
|
+
gitUrl: repoConfig.gitUrl,
|
|
175
|
+
})}) - ${commit.message}`,
|
|
170
176
|
);
|
|
171
177
|
});
|
|
172
178
|
}
|
|
@@ -15,7 +15,7 @@ import {type DeployResult} from '../git.js';
|
|
|
15
15
|
export type NotificationParams<Target extends NotificationTarget = NotificationTarget> = {
|
|
16
16
|
deployResult: Readonly<DeployResult>;
|
|
17
17
|
branchConfig: Readonly<DeployVirBranchConfig>;
|
|
18
|
-
repoConfig: Readonly<Pick<DeployVirRepoConfig, 'commitBaseUrl' | 'name'>>;
|
|
18
|
+
repoConfig: Readonly<Pick<DeployVirRepoConfig, 'commitBaseUrl' | 'gitUrl' | 'name'>>;
|
|
19
19
|
notification: Readonly<Extract<NotificationConfig, {target: Target}>>;
|
|
20
20
|
hookResult?: Readonly<PostAcceptHookResult> | undefined;
|
|
21
21
|
};
|
|
@@ -15,8 +15,19 @@ function truncateString(value: string, truncateAt = 100): string {
|
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
async function formatCommit(
|
|
19
|
-
|
|
18
|
+
async function formatCommit({
|
|
19
|
+
baseCommitUrl,
|
|
20
|
+
commit,
|
|
21
|
+
gitUrl,
|
|
22
|
+
}: Readonly<{
|
|
23
|
+
baseCommitUrl: string | undefined;
|
|
24
|
+
commit: Readonly<Commit>;
|
|
25
|
+
gitUrl: string;
|
|
26
|
+
}>) {
|
|
27
|
+
const authorName = await getCommitAuthorName({
|
|
28
|
+
commit,
|
|
29
|
+
gitUrl,
|
|
30
|
+
});
|
|
20
31
|
|
|
21
32
|
if (baseCommitUrl) {
|
|
22
33
|
return `<${joinUrlPaths(baseCommitUrl, commit.hash)}|${commit.hash.slice(0, 7)}> (${authorName}) ${truncateString(commit.message)}`;
|
|
@@ -68,12 +79,22 @@ export async function sendNotificationToSlack({
|
|
|
68
79
|
repoConfig,
|
|
69
80
|
}: Readonly<NotificationParams>) {
|
|
70
81
|
const deployedCommitBullets = await Promise.all(
|
|
71
|
-
deployedCommits.map(async (commit) =>
|
|
82
|
+
deployedCommits.map(async (commit) => {
|
|
83
|
+
return await formatCommit({
|
|
84
|
+
baseCommitUrl: repoConfig.commitBaseUrl,
|
|
85
|
+
commit,
|
|
86
|
+
gitUrl: repoConfig.gitUrl,
|
|
87
|
+
});
|
|
88
|
+
}),
|
|
72
89
|
);
|
|
73
90
|
const overwrittenCommitBullets = await Promise.all(
|
|
74
|
-
overwrittenCommits.map(
|
|
75
|
-
|
|
76
|
-
|
|
91
|
+
overwrittenCommits.map(async (commit) => {
|
|
92
|
+
return await formatCommit({
|
|
93
|
+
baseCommitUrl: repoConfig.commitBaseUrl,
|
|
94
|
+
commit,
|
|
95
|
+
gitUrl: repoConfig.gitUrl,
|
|
96
|
+
});
|
|
97
|
+
}),
|
|
77
98
|
);
|
|
78
99
|
|
|
79
100
|
const deployedCommitBulletLines = deployedCommitBullets.map((bullet) => `- ${bullet}`);
|