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 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 human author name for a commit. Commits authored by a bot (a name with a `[bot]`
11
- * suffix, like GitHub's merge queue bot) are attributed to their first non-bot `Co-authored-by:`
12
- * trailer instead. If the commit has no such trailer, its pull request's first non-bot assignee is
13
- * used (requires the `gh` CLI). All fallbacks resolve to the original bot name.
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: Readonly<Pick<Commit, 'author_name' | 'body'> & PartialWithUndefined<Pick<Commit, 'hash' | 'message'>>>): Promise<string>;
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 pullRequestAssigneeCache = new Map();
9
+ const pullRequestAuthorCache = new Map();
12
10
  /**
13
- * Extract the human author name for a commit. Commits authored by a bot (a name with a `[bot]`
14
- * suffix, like GitHub's merge queue bot) are attributed to their first non-bot `Co-authored-by:`
15
- * trailer instead. If the commit has no such trailer, its pull request's first non-bot assignee is
16
- * used (requires the `gh` CLI). All fallbacks resolve to the original bot name.
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
- if (!commit.author_name.endsWith(botNameSuffix)) {
22
- return commit.author_name;
23
- }
24
- const coAuthorName = Array.from(commit.body.matchAll(coAuthorRegExp))
25
- /** Strip the trailing `<email>` from each trailer. */
26
- .map((match) => (match[1] || '').split('<')[0]?.trim())
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 getPullRequestAssigneeName(commit) {
31
- const pullRequestNumber = commit.message?.match(pullRequestNumberRegExp)?.[1];
32
- const cacheKey = pullRequestNumber || commit.hash;
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
- const cached = pullRequestAssigneeCache.get(cacheKey);
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 = fetchPullRequestAssigneeName(pullRequestNumber
41
- ? `gh pr view ${pullRequestNumber} --json assignees`
42
- : `gh pr list --search ${cacheKey} --state merged --limit 1 --json assignees`);
43
- pullRequestAssigneeCache.set(cacheKey, pending);
40
+ const pending = fetchPullRequestAuthorName(buildPullRequestAuthorCommand({
41
+ gitUrl,
42
+ hash,
43
+ pullRequestNumber,
44
+ }));
45
+ pullRequestAuthorCache.set(cacheKey, pending);
44
46
  return pending;
45
47
  }
46
- async function fetchPullRequestAssigneeName(command) {
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 extractAssigneeName(JSON.parse(output.stdout));
69
+ return extractPullRequestAuthorName(JSON.parse(output.stdout));
52
70
  }
53
71
  catch (error) {
54
- log.faint(`Failed to read a pull request assignee: ${extractErrorMessage(error)}`);
72
+ log.faint(`Failed to read a pull request author: ${extractErrorMessage(error)}`);
55
73
  return undefined;
56
74
  }
57
75
  }
58
- function extractAssigneeName(parsedJson) {
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
- if (!check.hasKey(pullRequest, 'assignees') || !check.isArray(pullRequest.assignees)) {
62
- return undefined;
63
- }
64
- return pullRequest.assignees
65
- .map((assignee) => {
66
- const name = check.hasKey(assignee, 'name') && check.isString(assignee.name)
67
- ? assignee.name
68
- : '';
69
- const login = check.hasKey(assignee, 'login') && check.isString(assignee.login)
70
- ? assignee.login
71
- : '';
72
- return name || login;
73
- })
74
- .find((name) => name && !name.endsWith(botNameSuffix));
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(commit)}) - ${commit.message}`);
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(commit)}) - ${commit.message}`);
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(commit);
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) => await formatCommit(repoConfig.commitBaseUrl, commit)));
48
- const overwrittenCommitBullets = await Promise.all(overwrittenCommits.map(async (commit) => await formatCommit(repoConfig.commitBaseUrl, commit)));
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.5.0",
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": "bin.js",
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.2.2",
46
- "@augment-vir/common": "^32.2.2",
47
- "@augment-vir/node": "^32.2.2",
48
- "@inquirer/prompts": "^8.5.2",
49
- "@slack/web-api": "^8.0.0",
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.12",
53
- "url-vir": "^2.2.1"
54
+ "tsx": "^4.23.13",
55
+ "url-vir": "^2.2.2"
54
56
  },
55
57
  "devDependencies": {
56
- "@augment-vir/test": "^32.2.2",
57
- "@eslint/eslintrc": "^3.3.6",
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.2.0",
62
- "@typescript-eslint/eslint-plugin": "^8.66.0",
63
- "cspell": "^10.0.1",
64
- "dependency-cruiser": "^18.1.1",
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.8.1",
68
+ "eslint": "^10.10.0",
67
69
  "eslint-config-prettier": "^10.1.8",
68
- "eslint-plugin-jsdoc": "^64.0.2",
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": "^73.0.0",
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.2",
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.6",
88
+ "runstorm": "^1.1.0",
87
89
  "typedoc": "^0.28.20",
88
90
  "typescript": "^6.0.3",
89
- "typescript-eslint": "^8.66.0",
90
- "virmator": "^14.30.6"
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 pullRequestAssigneeCache = new Map<string, Promise<string | undefined>>();
19
+ const pullRequestAuthorCache = new Map<string, Promise<string | undefined>>();
22
20
 
23
21
  /**
24
- * Extract the human author name for a commit. Commits authored by a bot (a name with a `[bot]`
25
- * suffix, like GitHub's merge queue bot) are attributed to their first non-bot `Co-authored-by:`
26
- * trailer instead. If the commit has no such trailer, its pull request's first non-bot assignee is
27
- * used (requires the `gh` CLI). All fallbacks resolve to the original bot name.
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' | 'body'> &
34
- PartialWithUndefined<Pick<Commit, 'hash' | 'message'>>
35
- >,
36
- ): Promise<string> {
37
- if (!commit.author_name.endsWith(botNameSuffix)) {
38
- return commit.author_name;
39
- }
40
-
41
- const coAuthorName = Array.from(commit.body.matchAll(coAuthorRegExp))
42
- /** Strip the trailing `<email>` from each trailer. */
43
- .map((match) => (match[1] || '').split('<')[0]?.trim())
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 getPullRequestAssigneeName(
50
- commit: Readonly<PartialWithUndefined<Pick<Commit, 'hash' | 'message'>>>,
51
- ): Promise<string | undefined> {
52
- const pullRequestNumber = commit.message?.match(pullRequestNumberRegExp)?.[1];
53
- const cacheKey = pullRequestNumber || commit.hash;
54
-
55
- if (!cacheKey) {
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
- const cached = pullRequestAssigneeCache.get(cacheKey);
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 = fetchPullRequestAssigneeName(
66
- pullRequestNumber
67
- ? `gh pr view ${pullRequestNumber} --json assignees`
68
- : `gh pr list --search ${cacheKey} --state merged --limit 1 --json assignees`,
77
+ const pending = fetchPullRequestAuthorName(
78
+ buildPullRequestAuthorCommand({
79
+ gitUrl,
80
+ hash,
81
+ pullRequestNumber,
82
+ }),
69
83
  );
70
- pullRequestAssigneeCache.set(cacheKey, pending);
84
+ pullRequestAuthorCache.set(cacheKey, pending);
71
85
 
72
86
  return pending;
73
87
  }
74
88
 
75
- async function fetchPullRequestAssigneeName(command: string): Promise<string | undefined> {
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 extractAssigneeName(JSON.parse(output.stdout));
124
+ return extractPullRequestAuthorName(JSON.parse(output.stdout));
82
125
  } catch (error) {
83
- log.faint(`Failed to read a pull request assignee: ${extractErrorMessage(error)}`);
126
+ log.faint(`Failed to read a pull request author: ${extractErrorMessage(error)}`);
84
127
  return undefined;
85
128
  }
86
129
  }
87
130
 
88
- function extractAssigneeName(parsedJson: unknown): string | undefined {
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
- if (!check.hasKey(pullRequest, 'assignees') || !check.isArray(pullRequest.assignees)) {
93
- return undefined;
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 pullRequest.assignees
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(commit)}) - ${commit.message}`,
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(commit)}) - ${commit.message}`,
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(baseCommitUrl: string | undefined, commit: Readonly<Commit>) {
19
- const authorName = await getCommitAuthorName(commit);
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) => await formatCommit(repoConfig.commitBaseUrl, 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
- async (commit) => await formatCommit(repoConfig.commitBaseUrl, commit),
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}`);