deploy-vir 3.0.1 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/cli.js CHANGED
@@ -48,7 +48,7 @@ export async function runDeployVir(args, config) {
48
48
  const branchConfig = assertWrap.isDefined(repoConfig.deploys.find((deploy) => deploy.deployName === args.deployName, `Failed to find a deploy named '${args.deployName}' in repo '${args.repoName}'.`));
49
49
  const remoteName = await getGitRemoteName(git, repoConfig);
50
50
  log.faint(`Remote: ${remoteName}`);
51
- await pushDeploy(git, branchConfig, repoConfig, remoteName, config.notifications, args.bypassConfirmation);
51
+ await pushDeploy(git, branchConfig, repoConfig, remoteName, config.notifications, args.bypassConfirmation, config.hooks);
52
52
  }
53
53
  catch (error) {
54
54
  if (error instanceof KnownError) {
package/dist/config.d.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { type MaybePromise } from '@augment-vir/common';
2
+ import { type DeployResult } from './git.js';
1
3
  /**
2
4
  * The full deploy-vir config object.
3
5
  *
@@ -6,6 +8,32 @@
6
8
  export type DeployVirConfig = {
7
9
  repos: DeployVirRepoConfig[];
8
10
  notifications?: NotificationConfig[] | undefined;
11
+ hooks?: DeployVirHooks | undefined;
12
+ };
13
+ /**
14
+ * Hooks to run at various points during a deploy.
15
+ *
16
+ * @category Config
17
+ */
18
+ export type DeployVirHooks = {
19
+ /**
20
+ * Runs after the user accepts (or bypasses confirmation for) a deploy and the push has
21
+ * completed successfully. Fires once per branch deploy.
22
+ */
23
+ postAccept?: ((params: Readonly<PostAcceptHookParams>) => MaybePromise<void>) | undefined;
24
+ };
25
+ /**
26
+ * Params passed to a {@link DeployVirHooks.postAccept} hook.
27
+ *
28
+ * @category Config
29
+ */
30
+ export type PostAcceptHookParams = {
31
+ repoConfig: Readonly<DeployVirRepoConfig>;
32
+ branchConfig: Readonly<DeployVirBranchConfig>;
33
+ fromBranch: string;
34
+ toBranch: string;
35
+ remoteName: string;
36
+ deployResult: Readonly<DeployResult>;
9
37
  };
10
38
  /**
11
39
  * All available targets for sending notifications to.
@@ -52,6 +80,15 @@ export type SlackNotificationConfig = {
52
80
  * @category Config
53
81
  */
54
82
  export type NotificationConfig = SlackNotificationConfig;
83
+ /**
84
+ * A deploy-level notification override. Same as {@link NotificationConfig} but `webhookUrl` is
85
+ * optional — when omitted, the first matching top-level notification's `webhookUrl` is used.
86
+ *
87
+ * @category Config
88
+ */
89
+ export type DeployNotificationConfig = Omit<SlackNotificationConfig, 'webhookUrl'> & {
90
+ webhookUrl?: string | undefined;
91
+ };
55
92
  /**
56
93
  * A repo config for deploying.
57
94
  *
@@ -97,6 +134,13 @@ export type DeployVirBranchConfig = {
97
134
  enableNotifications?: boolean | undefined;
98
135
  }[];
99
136
  enableNotifications?: boolean | undefined;
137
+ /**
138
+ * Deploy-level notification overrides. When set, these are used instead of the top-level
139
+ * notifications. If a notification here omits `webhookUrl`, the first top-level notification
140
+ * with the same `target` provides it. Setting this also implicitly enables notifications for
141
+ * this deploy.
142
+ */
143
+ notifications?: DeployNotificationConfig[] | undefined;
100
144
  };
101
145
  /**
102
146
  * Define a type safe deploy-vir config.
package/dist/git.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { type DefaultLogFields, type ListLogLine, type SimpleGit } from 'simple-git';
2
- import { type DeployVirBranchConfig, type DeployVirRepoConfig, type NotificationConfig } from './config.js';
2
+ import { type DeployVirBranchConfig, type DeployVirHooks, type DeployVirRepoConfig, type NotificationConfig } from './config.js';
3
3
  /**
4
4
  * Find the git remote's name, or create a new one.
5
5
  *
@@ -45,4 +45,4 @@ export type DeployResult = {
45
45
  *
46
46
  * @category Internal
47
47
  */
48
- export declare function pushDeploy(git: Readonly<SimpleGit>, branchConfig: Readonly<DeployVirBranchConfig>, repoConfig: Readonly<DeployVirRepoConfig>, remoteName: string, notifications: ReadonlyArray<Readonly<NotificationConfig>> | undefined, bypassConfirmation?: boolean): Promise<DeployResult[]>;
48
+ export declare function pushDeploy(git: Readonly<SimpleGit>, branchConfig: Readonly<DeployVirBranchConfig>, repoConfig: Readonly<DeployVirRepoConfig>, remoteName: string, notifications: ReadonlyArray<Readonly<NotificationConfig>> | undefined, bypassConfirmation: boolean | undefined, hooks: Readonly<DeployVirHooks> | undefined): Promise<DeployResult[]>;
package/dist/git.js CHANGED
@@ -31,7 +31,7 @@ export async function getGitRemoteName(git, repoConfig) {
31
31
  *
32
32
  * @category Internal
33
33
  */
34
- export async function pushDeploy(git, branchConfig, repoConfig, remoteName, notifications, bypassConfirmation = false) {
34
+ export async function pushDeploy(git, branchConfig, repoConfig, remoteName, notifications, bypassConfirmation = false, hooks) {
35
35
  const { deployName, branches } = branchConfig;
36
36
  return await awaitedBlockingMap(branches, async ({ fromBranch, toBranch, enableNotifications }) => {
37
37
  assert.isTruthy(fromBranch, `Deploy '${deployName}' fromBranch cannot be empty.`);
@@ -100,11 +100,24 @@ export async function pushDeploy(git, branchConfig, repoConfig, remoteName, noti
100
100
  after: afterCommit,
101
101
  },
102
102
  };
103
- if (notifications?.length &&
104
- (repoConfig.enableNotifications ||
105
- enableNotifications ||
106
- branchConfig.enableNotifications)) {
107
- await sendNotifications(notifications, {
103
+ if (hooks?.postAccept) {
104
+ await hooks.postAccept({
105
+ branchConfig,
106
+ deployResult,
107
+ fromBranch,
108
+ remoteName,
109
+ repoConfig,
110
+ toBranch,
111
+ });
112
+ }
113
+ const resolvedNotifications = resolveNotifications({
114
+ branchConfig,
115
+ enableNotifications,
116
+ globalNotifications: notifications,
117
+ repoConfig,
118
+ });
119
+ if (resolvedNotifications.length) {
120
+ await sendNotifications(resolvedNotifications, {
108
121
  branchConfig,
109
122
  deployResult,
110
123
  repoConfig,
@@ -113,3 +126,28 @@ export async function pushDeploy(git, branchConfig, repoConfig, remoteName, noti
113
126
  return deployResult;
114
127
  });
115
128
  }
129
+ function resolveNotifications({ branchConfig, enableNotifications, globalNotifications, repoConfig, }) {
130
+ if (branchConfig.notifications?.length) {
131
+ return resolveDeployNotifications(branchConfig.notifications, globalNotifications);
132
+ }
133
+ const notificationsEnabled = repoConfig.enableNotifications || enableNotifications || branchConfig.enableNotifications;
134
+ if (notificationsEnabled && globalNotifications?.length) {
135
+ return [...globalNotifications];
136
+ }
137
+ return [];
138
+ }
139
+ function resolveDeployNotifications(deployNotifications, globalNotifications) {
140
+ return deployNotifications.map((deployNotification) => {
141
+ if (deployNotification.webhookUrl) {
142
+ return deployNotification;
143
+ }
144
+ const globalWebhookUrl = globalNotifications?.[0]?.webhookUrl;
145
+ if (!globalWebhookUrl) {
146
+ throw new KnownError(`Deploy notification for target '${deployNotification.target}' is missing webhookUrl and no top-level notification was found to fall back on.`);
147
+ }
148
+ return {
149
+ ...deployNotification,
150
+ webhookUrl: globalWebhookUrl,
151
+ };
152
+ });
153
+ }
@@ -27,7 +27,10 @@ function chunkSectionLines(lines) {
27
27
  const latestSection = sectionLines.slice(-1)[0];
28
28
  const currentSection = latestSection && latestSection.length + line.length < slackMaxSectionLength
29
29
  ? latestSection
30
- : pushAndGet(sectionLines, { length: 0, lines: [] });
30
+ : pushAndGet(sectionLines, {
31
+ length: 0,
32
+ lines: [],
33
+ });
31
34
  currentSection.length += line.length;
32
35
  currentSection.lines.push(line);
33
36
  });
@@ -107,7 +110,10 @@ export async function sendNotificationToSlack({ deployResult: { deployCommits: {
107
110
  ];
108
111
  await sendSlackMessage(notification, {
109
112
  attachments: [],
110
- channel: addPrefix({ value: notification.channelName, prefix: '#' }),
113
+ channel: addPrefix({
114
+ value: notification.channelName,
115
+ prefix: '#',
116
+ }),
111
117
  blocks,
112
118
  });
113
119
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deploy-vir",
3
- "version": "3.0.1",
3
+ "version": "3.2.0",
4
4
  "description": "Deploy a branch to another branch with notifications.",
5
5
  "keywords": [
6
6
  "deploy",
@@ -41,51 +41,51 @@
41
41
  "test:update": "npm run test update"
42
42
  },
43
43
  "dependencies": {
44
- "@augment-vir/assert": "^31.61.0",
45
- "@augment-vir/common": "^31.61.0",
46
- "@augment-vir/node": "^31.61.0",
47
- "@inquirer/prompts": "^8.2.1",
48
- "@slack/web-api": "^7.14.1",
44
+ "@augment-vir/assert": "^31.68.4",
45
+ "@augment-vir/common": "^31.68.4",
46
+ "@augment-vir/node": "^31.68.4",
47
+ "@inquirer/prompts": "^8.4.2",
48
+ "@slack/web-api": "^7.15.1",
49
49
  "cli-vir": "^1.0.3",
50
- "simple-git": "^3.31.1",
50
+ "simple-git": "^3.36.0",
51
51
  "tsx": "^4.21.0",
52
- "url-vir": "^2.1.7"
52
+ "url-vir": "^2.1.8"
53
53
  },
54
54
  "devDependencies": {
55
- "@eslint/eslintrc": "^3.3.3",
55
+ "@eslint/eslintrc": "^3.3.5",
56
56
  "@eslint/js": "^9.39.2",
57
- "@stylistic/eslint-plugin": "^5.8.0",
57
+ "@stylistic/eslint-plugin": "^5.10.0",
58
58
  "@stylistic/eslint-plugin-ts": "^4.4.1",
59
- "@types/node": "^25.2.3",
60
- "@typescript-eslint/eslint-plugin": "^8.56.0",
61
- "cspell": "^9.6.4",
62
- "dependency-cruiser": "^17.3.8",
63
- "esbuild": "^0.27.3",
59
+ "@types/node": "^25.6.0",
60
+ "@typescript-eslint/eslint-plugin": "^8.59.0",
61
+ "cspell": "^10.0.0",
62
+ "dependency-cruiser": "^17.3.10",
63
+ "esbuild": "^0.28.0",
64
64
  "eslint": "^9.39.2",
65
65
  "eslint-config-prettier": "^10.1.8",
66
- "eslint-plugin-jsdoc": "^62.5.0",
66
+ "eslint-plugin-jsdoc": "^62.9.0",
67
67
  "eslint-plugin-monorepo-cop": "^1.0.2",
68
- "eslint-plugin-playwright": "^2.5.1",
68
+ "eslint-plugin-playwright": "^2.10.2",
69
69
  "eslint-plugin-prettier": "^5.5.5",
70
70
  "eslint-plugin-require-extensions": "^0.1.3",
71
- "eslint-plugin-sonarjs": "^3.0.6",
72
- "eslint-plugin-unicorn": "^63.0.0",
71
+ "eslint-plugin-sonarjs": "^4.0.3",
72
+ "eslint-plugin-unicorn": "^64.0.0",
73
73
  "istanbul-smart-text-reporter": "^1.1.5",
74
- "markdown-code-example-inserter": "^3.0.3",
75
- "npm-check-updates": "^19.3.2",
74
+ "markdown-code-example-inserter": "^3.0.5",
75
+ "npm-check-updates": "^21.0.3",
76
76
  "prettier": "~3.3.3",
77
77
  "prettier-plugin-interpolated-html-tags": "^2.0.1",
78
78
  "prettier-plugin-jsdoc": "^1.8.0",
79
- "prettier-plugin-multiline-arrays": "^4.1.4",
79
+ "prettier-plugin-multiline-arrays": "^4.1.7",
80
80
  "prettier-plugin-organize-imports": "^4.3.0",
81
- "prettier-plugin-packagejson": "^3.0.0",
81
+ "prettier-plugin-packagejson": "^3.0.2",
82
82
  "prettier-plugin-sort-json": "^4.2.0",
83
83
  "prettier-plugin-toml": "^2.0.6",
84
84
  "runstorm": "^1.0.0",
85
- "typedoc": "^0.28.17",
85
+ "typedoc": "^0.28.19",
86
86
  "typescript": "^5.9.3",
87
- "typescript-eslint": "^8.56.0",
88
- "virmator": "^14.5.2"
87
+ "typescript-eslint": "^8.57.1",
88
+ "virmator": "^14.12.2"
89
89
  },
90
90
  "engines": {
91
91
  "node": ">=22"
package/src/cli/cli.ts CHANGED
@@ -112,6 +112,7 @@ export async function runDeployVir(
112
112
  remoteName,
113
113
  config.notifications,
114
114
  args.bypassConfirmation,
115
+ config.hooks,
115
116
  );
116
117
  } catch (error) {
117
118
  if (error instanceof KnownError) {
package/src/config.ts CHANGED
@@ -1,3 +1,6 @@
1
+ import {type MaybePromise} from '@augment-vir/common';
2
+ import {type DeployResult} from './git.js';
3
+
1
4
  /**
2
5
  * The full deploy-vir config object.
3
6
  *
@@ -6,6 +9,34 @@
6
9
  export type DeployVirConfig = {
7
10
  repos: DeployVirRepoConfig[];
8
11
  notifications?: NotificationConfig[] | undefined;
12
+ hooks?: DeployVirHooks | undefined;
13
+ };
14
+
15
+ /**
16
+ * Hooks to run at various points during a deploy.
17
+ *
18
+ * @category Config
19
+ */
20
+ export type DeployVirHooks = {
21
+ /**
22
+ * Runs after the user accepts (or bypasses confirmation for) a deploy and the push has
23
+ * completed successfully. Fires once per branch deploy.
24
+ */
25
+ postAccept?: ((params: Readonly<PostAcceptHookParams>) => MaybePromise<void>) | undefined;
26
+ };
27
+
28
+ /**
29
+ * Params passed to a {@link DeployVirHooks.postAccept} hook.
30
+ *
31
+ * @category Config
32
+ */
33
+ export type PostAcceptHookParams = {
34
+ repoConfig: Readonly<DeployVirRepoConfig>;
35
+ branchConfig: Readonly<DeployVirBranchConfig>;
36
+ fromBranch: string;
37
+ toBranch: string;
38
+ remoteName: string;
39
+ deployResult: Readonly<DeployResult>;
9
40
  };
10
41
 
11
42
  /**
@@ -57,6 +88,16 @@ export type SlackNotificationConfig = {
57
88
  // eslint-disable-next-line sonarjs/redundant-type-aliases
58
89
  export type NotificationConfig = SlackNotificationConfig;
59
90
 
91
+ /**
92
+ * A deploy-level notification override. Same as {@link NotificationConfig} but `webhookUrl` is
93
+ * optional — when omitted, the first matching top-level notification's `webhookUrl` is used.
94
+ *
95
+ * @category Config
96
+ */
97
+ export type DeployNotificationConfig = Omit<SlackNotificationConfig, 'webhookUrl'> & {
98
+ webhookUrl?: string | undefined;
99
+ };
100
+
60
101
  /**
61
102
  * A repo config for deploying.
62
103
  *
@@ -103,6 +144,13 @@ export type DeployVirBranchConfig = {
103
144
  enableNotifications?: boolean | undefined;
104
145
  }[];
105
146
  enableNotifications?: boolean | undefined;
147
+ /**
148
+ * Deploy-level notification overrides. When set, these are used instead of the top-level
149
+ * notifications. If a notification here omits `webhookUrl`, the first top-level notification
150
+ * with the same `target` provides it. Setting this also implicitly enables notifications for
151
+ * this deploy.
152
+ */
153
+ notifications?: DeployNotificationConfig[] | undefined;
106
154
  };
107
155
 
108
156
  /**
package/src/git.ts CHANGED
@@ -3,7 +3,9 @@ import {awaitedBlockingMap, log, logColors} from '@augment-vir/common';
3
3
  import {confirm} from '@inquirer/prompts';
4
4
  import {type DefaultLogFields, type ListLogLine, type SimpleGit} from 'simple-git';
5
5
  import {
6
+ type DeployNotificationConfig,
6
7
  type DeployVirBranchConfig,
8
+ type DeployVirHooks,
7
9
  type DeployVirRepoConfig,
8
10
  type NotificationConfig,
9
11
  } from './config.js';
@@ -99,6 +101,7 @@ export async function pushDeploy(
99
101
  remoteName: string,
100
102
  notifications: ReadonlyArray<Readonly<NotificationConfig>> | undefined,
101
103
  bypassConfirmation = false,
104
+ hooks: Readonly<DeployVirHooks> | undefined,
102
105
  ): Promise<DeployResult[]> {
103
106
  const {deployName, branches} = branchConfig;
104
107
  return await awaitedBlockingMap(
@@ -208,13 +211,26 @@ export async function pushDeploy(
208
211
  },
209
212
  };
210
213
 
211
- if (
212
- notifications?.length &&
213
- (repoConfig.enableNotifications ||
214
- enableNotifications ||
215
- branchConfig.enableNotifications)
216
- ) {
217
- await sendNotifications(notifications, {
214
+ if (hooks?.postAccept) {
215
+ await hooks.postAccept({
216
+ branchConfig,
217
+ deployResult,
218
+ fromBranch,
219
+ remoteName,
220
+ repoConfig,
221
+ toBranch,
222
+ });
223
+ }
224
+
225
+ const resolvedNotifications = resolveNotifications({
226
+ branchConfig,
227
+ enableNotifications,
228
+ globalNotifications: notifications,
229
+ repoConfig,
230
+ });
231
+
232
+ if (resolvedNotifications.length) {
233
+ await sendNotifications(resolvedNotifications, {
218
234
  branchConfig,
219
235
  deployResult,
220
236
  repoConfig,
@@ -225,3 +241,52 @@ export async function pushDeploy(
225
241
  },
226
242
  );
227
243
  }
244
+
245
+ function resolveNotifications({
246
+ branchConfig,
247
+ enableNotifications,
248
+ globalNotifications,
249
+ repoConfig,
250
+ }: Readonly<{
251
+ branchConfig: Readonly<DeployVirBranchConfig>;
252
+ enableNotifications: boolean | undefined;
253
+ globalNotifications: ReadonlyArray<Readonly<NotificationConfig>> | undefined;
254
+ repoConfig: Readonly<DeployVirRepoConfig>;
255
+ }>): NotificationConfig[] {
256
+ if (branchConfig.notifications?.length) {
257
+ return resolveDeployNotifications(branchConfig.notifications, globalNotifications);
258
+ }
259
+
260
+ const notificationsEnabled =
261
+ repoConfig.enableNotifications || enableNotifications || branchConfig.enableNotifications;
262
+
263
+ if (notificationsEnabled && globalNotifications?.length) {
264
+ return [...globalNotifications];
265
+ }
266
+
267
+ return [];
268
+ }
269
+
270
+ function resolveDeployNotifications(
271
+ deployNotifications: ReadonlyArray<Readonly<DeployNotificationConfig>>,
272
+ globalNotifications: ReadonlyArray<Readonly<NotificationConfig>> | undefined,
273
+ ): NotificationConfig[] {
274
+ return deployNotifications.map((deployNotification): NotificationConfig => {
275
+ if (deployNotification.webhookUrl) {
276
+ return deployNotification as NotificationConfig;
277
+ }
278
+
279
+ const globalWebhookUrl = globalNotifications?.[0]?.webhookUrl;
280
+
281
+ if (!globalWebhookUrl) {
282
+ throw new KnownError(
283
+ `Deploy notification for target '${deployNotification.target}' is missing webhookUrl and no top-level notification was found to fall back on.`,
284
+ );
285
+ }
286
+
287
+ return {
288
+ ...deployNotification,
289
+ webhookUrl: globalWebhookUrl,
290
+ };
291
+ });
292
+ }
@@ -39,7 +39,10 @@ function chunkSectionLines(lines: ReadonlyArray<string>): string[][] {
39
39
  const currentSection =
40
40
  latestSection && latestSection.length + line.length < slackMaxSectionLength
41
41
  ? latestSection
42
- : pushAndGet(sectionLines, {length: 0, lines: []});
42
+ : pushAndGet(sectionLines, {
43
+ length: 0,
44
+ lines: [],
45
+ });
43
46
  currentSection.length += line.length;
44
47
  currentSection.lines.push(line);
45
48
  });
@@ -139,7 +142,10 @@ export async function sendNotificationToSlack({
139
142
 
140
143
  await sendSlackMessage(notification, {
141
144
  attachments: [],
142
- channel: addPrefix({value: notification.channelName, prefix: '#'}),
145
+ channel: addPrefix({
146
+ value: notification.channelName,
147
+ prefix: '#',
148
+ }),
143
149
  blocks,
144
150
  });
145
151
  }