@bonvoy/plugin-git 0.4.0 → 0.6.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/README.md +7 -4
- package/dist/index.d.mts +7 -0
- package/dist/index.mjs +101 -8
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -25,9 +25,9 @@ npm install @bonvoy/plugin-git
|
|
|
25
25
|
// bonvoy.config.js
|
|
26
26
|
export default {
|
|
27
27
|
git: {
|
|
28
|
-
commitMessage: 'chore
|
|
29
|
-
tagFormat: '{name}@{version}',
|
|
30
|
-
push: true,
|
|
28
|
+
commitMessage: 'chore: :bookmark: release', // default
|
|
29
|
+
tagFormat: '{name}@{version}', // default
|
|
30
|
+
push: true, // default
|
|
31
31
|
},
|
|
32
32
|
};
|
|
33
33
|
```
|
|
@@ -36,10 +36,13 @@ export default {
|
|
|
36
36
|
|
|
37
37
|
| Placeholder | Description |
|
|
38
38
|
|-------------|-------------|
|
|
39
|
-
| `{packages}` | Comma-separated list of released package names |
|
|
39
|
+
| `{packages}` | Comma-separated list of released package names (for subject) |
|
|
40
|
+
| `{details}` | Package list with versions, one per line (for body) |
|
|
40
41
|
| `{name}` | Package name (for tag format) |
|
|
41
42
|
| `{version}` | Package version (for tag format) |
|
|
42
43
|
|
|
44
|
+
> **Note:** If neither `{details}` is used in the commit message, package details are automatically appended as the commit body.
|
|
45
|
+
|
|
43
46
|
## Hooks
|
|
44
47
|
|
|
45
48
|
This plugin taps into the following hooks:
|
package/dist/index.d.mts
CHANGED
|
@@ -18,6 +18,11 @@ interface GitOperations {
|
|
|
18
18
|
files: string[];
|
|
19
19
|
}>>;
|
|
20
20
|
getLastTag(cwd: string): Promise<string | null>;
|
|
21
|
+
getHeadSha(cwd: string): Promise<string>;
|
|
22
|
+
resetHard(sha: string, cwd: string): Promise<void>;
|
|
23
|
+
deleteTag(name: string, cwd: string): Promise<void>;
|
|
24
|
+
deleteRemoteTags(tags: string[], cwd: string): Promise<void>;
|
|
25
|
+
forcePush(cwd: string, branch: string): Promise<void>;
|
|
21
26
|
}
|
|
22
27
|
declare const defaultGitOperations: GitOperations;
|
|
23
28
|
//#endregion
|
|
@@ -36,11 +41,13 @@ declare class GitPlugin implements BonvoyPlugin {
|
|
|
36
41
|
hooks: {
|
|
37
42
|
validateRepo: any;
|
|
38
43
|
beforePublish: any;
|
|
44
|
+
rollback: any;
|
|
39
45
|
};
|
|
40
46
|
}): void;
|
|
41
47
|
private commitChanges;
|
|
42
48
|
private createTags;
|
|
43
49
|
private pushChanges;
|
|
50
|
+
private rollback;
|
|
44
51
|
private validateTags;
|
|
45
52
|
}
|
|
46
53
|
//#endregion
|
package/dist/index.mjs
CHANGED
|
@@ -88,6 +88,40 @@ const defaultGitOperations = {
|
|
|
88
88
|
});
|
|
89
89
|
}
|
|
90
90
|
return commits;
|
|
91
|
+
},
|
|
92
|
+
async getHeadSha(cwd) {
|
|
93
|
+
const { stdout } = await execa("git", ["rev-parse", "HEAD"], { cwd });
|
|
94
|
+
return stdout.trim();
|
|
95
|
+
},
|
|
96
|
+
async resetHard(sha, cwd) {
|
|
97
|
+
await execa("git", [
|
|
98
|
+
"reset",
|
|
99
|
+
"--hard",
|
|
100
|
+
sha
|
|
101
|
+
], { cwd });
|
|
102
|
+
},
|
|
103
|
+
async deleteTag(name, cwd) {
|
|
104
|
+
await execa("git", [
|
|
105
|
+
"tag",
|
|
106
|
+
"-d",
|
|
107
|
+
name
|
|
108
|
+
], { cwd });
|
|
109
|
+
},
|
|
110
|
+
async deleteRemoteTags(tags, cwd) {
|
|
111
|
+
for (const tag of tags) await execa("git", [
|
|
112
|
+
"push",
|
|
113
|
+
"--delete",
|
|
114
|
+
"origin",
|
|
115
|
+
tag
|
|
116
|
+
], { cwd });
|
|
117
|
+
},
|
|
118
|
+
async forcePush(cwd, branch) {
|
|
119
|
+
await execa("git", [
|
|
120
|
+
"push",
|
|
121
|
+
"--force-with-lease",
|
|
122
|
+
"origin",
|
|
123
|
+
branch
|
|
124
|
+
], { cwd });
|
|
91
125
|
}
|
|
92
126
|
};
|
|
93
127
|
|
|
@@ -99,7 +133,7 @@ var GitPlugin = class {
|
|
|
99
133
|
ops;
|
|
100
134
|
constructor(config = {}, ops) {
|
|
101
135
|
this.config = {
|
|
102
|
-
commitMessage: config.commitMessage ?? "chore: release
|
|
136
|
+
commitMessage: config.commitMessage ?? "chore: :bookmark: release",
|
|
103
137
|
tagFormat: config.tagFormat ?? "{name}@{version}",
|
|
104
138
|
push: config.push ?? true
|
|
105
139
|
};
|
|
@@ -119,33 +153,92 @@ var GitPlugin = class {
|
|
|
119
153
|
await this.pushChanges(context);
|
|
120
154
|
}
|
|
121
155
|
});
|
|
156
|
+
bonvoy.hooks.rollback.tapPromise(this.name, async (context) => {
|
|
157
|
+
await this.rollback(context);
|
|
158
|
+
});
|
|
122
159
|
}
|
|
123
160
|
async commitChanges(context) {
|
|
124
|
-
const { packages, isDryRun, rootPath, logger } = context;
|
|
161
|
+
const { packages, isDryRun, rootPath, logger, actionLog } = context;
|
|
125
162
|
if (packages.length === 0) return;
|
|
163
|
+
const packageList = packages.map((pkg) => `- ${pkg.name}@${pkg.version}`).join("\n");
|
|
126
164
|
const packageNames = packages.map((pkg) => pkg.name).join(", ");
|
|
127
|
-
const message = this.config.commitMessage.replace("{packages}", packageNames);
|
|
128
|
-
|
|
165
|
+
const message = this.config.commitMessage.replace("{packages}", packageNames).replace("{details}", packageList);
|
|
166
|
+
const fullMessage = message.includes(packageList) ? message : `${message}\n\n${packageList}`;
|
|
167
|
+
logger.info(` Commit message: "${fullMessage}"`);
|
|
129
168
|
if (!isDryRun) {
|
|
169
|
+
const previousSha = await this.ops.getHeadSha(rootPath);
|
|
130
170
|
await this.ops.add(".", rootPath);
|
|
131
|
-
await this.ops.commit(
|
|
171
|
+
await this.ops.commit(fullMessage, rootPath);
|
|
172
|
+
actionLog.record({
|
|
173
|
+
plugin: "git",
|
|
174
|
+
action: "commit",
|
|
175
|
+
data: { previousSha }
|
|
176
|
+
});
|
|
132
177
|
}
|
|
133
178
|
}
|
|
134
179
|
async createTags(context) {
|
|
135
|
-
const { packages, isDryRun, rootPath, logger } = context;
|
|
180
|
+
const { packages, isDryRun, rootPath, logger, actionLog } = context;
|
|
181
|
+
const tags = [];
|
|
136
182
|
for (const pkg of packages) {
|
|
137
183
|
const tag = this.config.tagFormat.replace("{name}", pkg.name).replace("{version}", pkg.version);
|
|
138
184
|
logger.info(` Tag: ${tag}`);
|
|
139
|
-
if (!isDryRun)
|
|
185
|
+
if (!isDryRun) {
|
|
186
|
+
await this.ops.tag(tag, rootPath);
|
|
187
|
+
tags.push(tag);
|
|
188
|
+
}
|
|
140
189
|
}
|
|
190
|
+
if (tags.length > 0) actionLog.record({
|
|
191
|
+
plugin: "git",
|
|
192
|
+
action: "tag",
|
|
193
|
+
data: { tags }
|
|
194
|
+
});
|
|
141
195
|
}
|
|
142
196
|
async pushChanges(context) {
|
|
143
|
-
const { packages, isDryRun, rootPath, logger } = context;
|
|
197
|
+
const { packages, isDryRun, rootPath, logger, actionLog } = context;
|
|
144
198
|
logger.info(" Pushing commits and tags...");
|
|
145
199
|
if (!isDryRun) {
|
|
200
|
+
const branch = await this.ops.getCurrentBranch(rootPath);
|
|
146
201
|
await this.ops.push(rootPath);
|
|
202
|
+
actionLog.record({
|
|
203
|
+
plugin: "git",
|
|
204
|
+
action: "push",
|
|
205
|
+
data: { branch }
|
|
206
|
+
});
|
|
147
207
|
const tags = packages.map((pkg) => this.config.tagFormat.replace("{name}", pkg.name).replace("{version}", pkg.version));
|
|
148
208
|
await this.ops.pushTags(tags, rootPath);
|
|
209
|
+
actionLog.record({
|
|
210
|
+
plugin: "git",
|
|
211
|
+
action: "pushTags",
|
|
212
|
+
data: { tags }
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
async rollback(context) {
|
|
217
|
+
const { rootPath, logger } = context;
|
|
218
|
+
const actions = context.actions.filter((a) => a.plugin === "git").reverse();
|
|
219
|
+
for (const action of actions) try {
|
|
220
|
+
if (action.action === "pushTags") {
|
|
221
|
+
const tags = action.data.tags;
|
|
222
|
+
logger.info(` ↩️ Deleting remote tags: ${tags.join(", ")}`);
|
|
223
|
+
await this.ops.deleteRemoteTags(tags, rootPath);
|
|
224
|
+
} else if (action.action === "push") {
|
|
225
|
+
const branch = action.data.branch;
|
|
226
|
+
logger.info(` ↩️ Force-pushing ${branch} to previous state`);
|
|
227
|
+
await this.ops.forcePush(rootPath, branch);
|
|
228
|
+
} else if (action.action === "tag") {
|
|
229
|
+
const tags = action.data.tags;
|
|
230
|
+
for (const tag of tags) {
|
|
231
|
+
logger.info(` ↩️ Deleting local tag: ${tag}`);
|
|
232
|
+
await this.ops.deleteTag(tag, rootPath);
|
|
233
|
+
}
|
|
234
|
+
} else if (action.action === "commit") {
|
|
235
|
+
const sha = action.data.previousSha;
|
|
236
|
+
logger.info(` ↩️ Resetting to ${sha}`);
|
|
237
|
+
await this.ops.resetHard(sha, rootPath);
|
|
238
|
+
}
|
|
239
|
+
} catch (error) {
|
|
240
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
241
|
+
logger.warn(` ⚠️ Failed to rollback git ${action.action}: ${msg}`);
|
|
149
242
|
}
|
|
150
243
|
}
|
|
151
244
|
async validateTags(context) {
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../src/operations.ts","../src/git.ts"],"sourcesContent":["import { execa } from 'execa';\n\nexport interface GitOperations {\n add(files: string, cwd: string): Promise<void>;\n commit(message: string, cwd: string): Promise<void>;\n tag(name: string, cwd: string): Promise<void>;\n push(cwd: string, branch?: string): Promise<void>;\n pushTags(tags: string[], cwd: string): Promise<void>;\n checkout(branch: string, cwd: string, create?: boolean): Promise<void>;\n getCurrentBranch(cwd: string): Promise<string>;\n tagExists(name: string, cwd: string): Promise<boolean>;\n getCommitsSinceTag(\n tag: string | null,\n cwd: string,\n ): Promise<\n Array<{ hash: string; message: string; author: string; date: string; files: string[] }>\n >;\n getLastTag(cwd: string): Promise<string | null>;\n}\n\nexport const defaultGitOperations: GitOperations = {\n async add(files, cwd) {\n await execa('git', ['add', files], { cwd });\n },\n\n async commit(message, cwd) {\n await execa('git', ['commit', '-m', message], { cwd });\n },\n\n async tag(name, cwd) {\n await execa('git', ['tag', name], { cwd });\n },\n\n /* c8 ignore start - real git operations */\n async push(cwd, branch?) {\n if (branch) {\n await execa('git', ['push', '-u', 'origin', branch], { cwd });\n } else {\n await execa('git', ['push'], { cwd });\n }\n },\n\n async pushTags(tags, cwd) {\n await execa('git', ['push', 'origin', ...tags], { cwd });\n },\n\n async checkout(branch, cwd, create = false) {\n const args = create ? ['checkout', '-b', branch] : ['checkout', branch];\n await execa('git', args, { cwd });\n },\n\n async getCurrentBranch(cwd) {\n const { stdout } = await execa('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd });\n return stdout.trim();\n },\n\n async tagExists(name, cwd) {\n try {\n await execa('git', ['rev-parse', `refs/tags/${name}`], { cwd });\n return true;\n } catch {\n return false;\n }\n },\n /* c8 ignore stop */\n\n async getLastTag(cwd) {\n try {\n const { stdout } = await execa('git', ['describe', '--tags', '--abbrev=0'], { cwd });\n return stdout.trim() || null;\n } catch {\n return null;\n }\n },\n\n async getCommitsSinceTag(tag, cwd) {\n const range = tag ? `${tag}..HEAD` : 'HEAD';\n const { stdout } = await execa(\n 'git',\n ['log', '--pretty=format:%H|%s|%an|%aI', '--name-only', range],\n { cwd },\n );\n\n if (!stdout.trim()) return [];\n\n const commits: Array<{\n hash: string;\n message: string;\n author: string;\n date: string;\n files: string[];\n }> = [];\n const entries = stdout.split('\\n\\n');\n\n for (const entry of entries) {\n const lines = entry.split('\\n');\n const [firstLine, ...fileLines] = lines;\n const [hash, message, author, date] = firstLine.split('|');\n if (hash && message) {\n commits.push({\n hash,\n message,\n author: author || '',\n date: date || '',\n files: fileLines.filter((f) => f.trim()),\n });\n }\n }\n\n return commits;\n },\n};\n","import type { BonvoyPlugin, Context, PublishContext } from '@bonvoy/core';\n\nimport { defaultGitOperations, type GitOperations } from './operations.js';\n\nexport interface GitPluginConfig {\n commitMessage?: string;\n tagFormat?: string;\n push?: boolean;\n}\n\nexport default class GitPlugin implements BonvoyPlugin {\n name = 'git';\n\n private config: Required<GitPluginConfig>;\n private ops: GitOperations;\n\n constructor(config: GitPluginConfig = {}, ops?: GitOperations) {\n this.config = {\n commitMessage: config.commitMessage ?? 'chore: release {packages}',\n tagFormat: config.tagFormat ?? '{name}@{version}',\n push: config.push ?? true,\n };\n this.ops = ops ?? defaultGitOperations;\n }\n\n // biome-ignore lint/suspicious/noExplicitAny: Hook types are complex and vary by implementation\n apply(bonvoy: { hooks: { validateRepo: any; beforePublish: any } }): void {\n bonvoy.hooks.validateRepo.tapPromise(this.name, async (context: Context) => {\n await this.validateTags(context);\n });\n\n bonvoy.hooks.beforePublish.tapPromise(this.name, async (context: PublishContext) => {\n context.logger.info('📝 Committing changes...');\n await this.commitChanges(context);\n context.logger.info('🏷️ Creating git tags...');\n await this.createTags(context);\n\n if (this.config.push) {\n context.logger.info('⬆️ Pushing to remote...');\n await this.pushChanges(context);\n }\n });\n }\n\n private async commitChanges(context: PublishContext): Promise<void> {\n const { packages, isDryRun, rootPath, logger } = context;\n\n if (packages.length === 0) return;\n\n const packageNames = packages.map((pkg) => pkg.name).join(', ');\n const message = this.config.commitMessage.replace('{packages}', packageNames);\n\n logger.info(` Commit message: \"${message}\"`);\n\n if (!isDryRun) {\n await this.ops.add('.', rootPath);\n await this.ops.commit(message, rootPath);\n }\n }\n\n private async createTags(context: PublishContext): Promise<void> {\n const { packages, isDryRun, rootPath, logger } = context;\n\n for (const pkg of packages) {\n const tag = this.config.tagFormat\n .replace('{name}', pkg.name)\n .replace('{version}', pkg.version);\n\n logger.info(` Tag: ${tag}`);\n\n if (!isDryRun) {\n await this.ops.tag(tag, rootPath);\n }\n }\n }\n\n private async pushChanges(context: PublishContext): Promise<void> {\n const { packages, isDryRun, rootPath, logger } = context;\n\n logger.info(' Pushing commits and tags...');\n\n if (!isDryRun) {\n await this.ops.push(rootPath);\n\n const tags = packages.map((pkg) =>\n this.config.tagFormat.replace('{name}', pkg.name).replace('{version}', pkg.version),\n );\n await this.ops.pushTags(tags, rootPath);\n }\n }\n\n private async validateTags(context: Context): Promise<void> {\n const { changedPackages, versions, rootPath, logger } = context;\n if (!versions) return;\n\n const existingTags: string[] = [];\n\n for (const pkg of changedPackages) {\n const version = versions[pkg.name];\n if (!version) continue;\n\n const tag = this.config.tagFormat.replace('{name}', pkg.name).replace('{version}', version);\n\n if (await this.ops.tagExists(tag, rootPath)) {\n existingTags.push(tag);\n }\n }\n\n if (existingTags.length > 0) {\n logger.error(`❌ Git tags already exist: ${existingTags.join(', ')}`);\n throw new Error(\n `Cannot release: git tags already exist (${existingTags.join(', ')}). Delete them first or bump to a new version.`,\n );\n }\n }\n}\n\nexport { defaultGitOperations, type GitOperations } from './operations.js';\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/operations.ts","../src/git.ts"],"sourcesContent":["import { execa } from 'execa';\n\nexport interface GitOperations {\n add(files: string, cwd: string): Promise<void>;\n commit(message: string, cwd: string): Promise<void>;\n tag(name: string, cwd: string): Promise<void>;\n push(cwd: string, branch?: string): Promise<void>;\n pushTags(tags: string[], cwd: string): Promise<void>;\n checkout(branch: string, cwd: string, create?: boolean): Promise<void>;\n getCurrentBranch(cwd: string): Promise<string>;\n tagExists(name: string, cwd: string): Promise<boolean>;\n getCommitsSinceTag(\n tag: string | null,\n cwd: string,\n ): Promise<\n Array<{ hash: string; message: string; author: string; date: string; files: string[] }>\n >;\n getLastTag(cwd: string): Promise<string | null>;\n // Rollback operations\n getHeadSha(cwd: string): Promise<string>;\n resetHard(sha: string, cwd: string): Promise<void>;\n deleteTag(name: string, cwd: string): Promise<void>;\n deleteRemoteTags(tags: string[], cwd: string): Promise<void>;\n forcePush(cwd: string, branch: string): Promise<void>;\n}\n\nexport const defaultGitOperations: GitOperations = {\n async add(files, cwd) {\n await execa('git', ['add', files], { cwd });\n },\n\n async commit(message, cwd) {\n await execa('git', ['commit', '-m', message], { cwd });\n },\n\n async tag(name, cwd) {\n await execa('git', ['tag', name], { cwd });\n },\n\n /* c8 ignore start - real git operations */\n async push(cwd, branch?) {\n if (branch) {\n await execa('git', ['push', '-u', 'origin', branch], { cwd });\n } else {\n await execa('git', ['push'], { cwd });\n }\n },\n\n async pushTags(tags, cwd) {\n await execa('git', ['push', 'origin', ...tags], { cwd });\n },\n\n async checkout(branch, cwd, create = false) {\n const args = create ? ['checkout', '-b', branch] : ['checkout', branch];\n await execa('git', args, { cwd });\n },\n\n async getCurrentBranch(cwd) {\n const { stdout } = await execa('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd });\n return stdout.trim();\n },\n\n async tagExists(name, cwd) {\n try {\n await execa('git', ['rev-parse', `refs/tags/${name}`], { cwd });\n return true;\n } catch {\n return false;\n }\n },\n /* c8 ignore stop */\n\n async getLastTag(cwd) {\n try {\n const { stdout } = await execa('git', ['describe', '--tags', '--abbrev=0'], { cwd });\n return stdout.trim() || null;\n } catch {\n return null;\n }\n },\n\n async getCommitsSinceTag(tag, cwd) {\n const range = tag ? `${tag}..HEAD` : 'HEAD';\n const { stdout } = await execa(\n 'git',\n ['log', '--pretty=format:%H|%s|%an|%aI', '--name-only', range],\n { cwd },\n );\n\n if (!stdout.trim()) return [];\n\n const commits: Array<{\n hash: string;\n message: string;\n author: string;\n date: string;\n files: string[];\n }> = [];\n const entries = stdout.split('\\n\\n');\n\n for (const entry of entries) {\n const lines = entry.split('\\n');\n const [firstLine, ...fileLines] = lines;\n const [hash, message, author, date] = firstLine.split('|');\n if (hash && message) {\n commits.push({\n hash,\n message,\n author: author || '',\n date: date || '',\n files: fileLines.filter((f) => f.trim()),\n });\n }\n }\n\n return commits;\n },\n\n /* c8 ignore start - real git operations */\n async getHeadSha(cwd) {\n const { stdout } = await execa('git', ['rev-parse', 'HEAD'], { cwd });\n return stdout.trim();\n },\n\n async resetHard(sha, cwd) {\n await execa('git', ['reset', '--hard', sha], { cwd });\n },\n\n async deleteTag(name, cwd) {\n await execa('git', ['tag', '-d', name], { cwd });\n },\n\n async deleteRemoteTags(tags, cwd) {\n for (const tag of tags) {\n await execa('git', ['push', '--delete', 'origin', tag], { cwd });\n }\n },\n\n async forcePush(cwd, branch) {\n await execa('git', ['push', '--force-with-lease', 'origin', branch], { cwd });\n },\n /* c8 ignore stop */\n};\n","import type { BonvoyPlugin, Context, PublishContext, RollbackContext } from '@bonvoy/core';\n\nimport { defaultGitOperations, type GitOperations } from './operations.js';\n\nexport interface GitPluginConfig {\n commitMessage?: string;\n tagFormat?: string;\n push?: boolean;\n}\n\nexport default class GitPlugin implements BonvoyPlugin {\n name = 'git';\n\n private config: Required<GitPluginConfig>;\n private ops: GitOperations;\n\n constructor(config: GitPluginConfig = {}, ops?: GitOperations) {\n this.config = {\n commitMessage: config.commitMessage ?? 'chore: :bookmark: release',\n tagFormat: config.tagFormat ?? '{name}@{version}',\n push: config.push ?? true,\n };\n this.ops = ops ?? defaultGitOperations;\n }\n\n // biome-ignore lint/suspicious/noExplicitAny: Hook types are complex and vary by implementation\n apply(bonvoy: { hooks: { validateRepo: any; beforePublish: any; rollback: any } }): void {\n bonvoy.hooks.validateRepo.tapPromise(this.name, async (context: Context) => {\n await this.validateTags(context);\n });\n\n bonvoy.hooks.beforePublish.tapPromise(this.name, async (context: PublishContext) => {\n context.logger.info('📝 Committing changes...');\n await this.commitChanges(context);\n context.logger.info('🏷️ Creating git tags...');\n await this.createTags(context);\n\n if (this.config.push) {\n context.logger.info('⬆️ Pushing to remote...');\n await this.pushChanges(context);\n }\n });\n\n bonvoy.hooks.rollback.tapPromise(this.name, async (context: RollbackContext) => {\n await this.rollback(context);\n });\n }\n\n private async commitChanges(context: PublishContext): Promise<void> {\n const { packages, isDryRun, rootPath, logger, actionLog } = context;\n\n if (packages.length === 0) return;\n\n const packageList = packages.map((pkg) => `- ${pkg.name}@${pkg.version}`).join('\\n');\n const packageNames = packages.map((pkg) => pkg.name).join(', ');\n const message = this.config.commitMessage\n .replace('{packages}', packageNames)\n .replace('{details}', packageList);\n\n // Append package details as commit body if not already included via {details}\n const fullMessage = message.includes(packageList) ? message : `${message}\\n\\n${packageList}`;\n\n logger.info(` Commit message: \"${fullMessage}\"`);\n\n if (!isDryRun) {\n const previousSha = await this.ops.getHeadSha(rootPath);\n await this.ops.add('.', rootPath);\n await this.ops.commit(fullMessage, rootPath);\n actionLog.record({ plugin: 'git', action: 'commit', data: { previousSha } });\n }\n }\n\n private async createTags(context: PublishContext): Promise<void> {\n const { packages, isDryRun, rootPath, logger, actionLog } = context;\n const tags: string[] = [];\n\n for (const pkg of packages) {\n const tag = this.config.tagFormat\n .replace('{name}', pkg.name)\n .replace('{version}', pkg.version);\n\n logger.info(` Tag: ${tag}`);\n\n if (!isDryRun) {\n await this.ops.tag(tag, rootPath);\n tags.push(tag);\n }\n }\n\n if (tags.length > 0) {\n actionLog.record({ plugin: 'git', action: 'tag', data: { tags } });\n }\n }\n\n private async pushChanges(context: PublishContext): Promise<void> {\n const { packages, isDryRun, rootPath, logger, actionLog } = context;\n\n logger.info(' Pushing commits and tags...');\n\n if (!isDryRun) {\n const branch = await this.ops.getCurrentBranch(rootPath);\n await this.ops.push(rootPath);\n actionLog.record({ plugin: 'git', action: 'push', data: { branch } });\n\n const tags = packages.map((pkg) =>\n this.config.tagFormat.replace('{name}', pkg.name).replace('{version}', pkg.version),\n );\n await this.ops.pushTags(tags, rootPath);\n actionLog.record({ plugin: 'git', action: 'pushTags', data: { tags } });\n }\n }\n\n private async rollback(context: RollbackContext): Promise<void> {\n const { rootPath, logger } = context;\n const actions = context.actions.filter((a) => a.plugin === 'git').reverse();\n\n for (const action of actions) {\n try {\n if (action.action === 'pushTags') {\n const tags = action.data.tags as string[];\n logger.info(` ↩️ Deleting remote tags: ${tags.join(', ')}`);\n await this.ops.deleteRemoteTags(tags, rootPath);\n } else if (action.action === 'push') {\n const branch = action.data.branch as string;\n logger.info(` ↩️ Force-pushing ${branch} to previous state`);\n await this.ops.forcePush(rootPath, branch);\n } else if (action.action === 'tag') {\n const tags = action.data.tags as string[];\n for (const tag of tags) {\n logger.info(` ↩️ Deleting local tag: ${tag}`);\n await this.ops.deleteTag(tag, rootPath);\n }\n } else if (action.action === 'commit') {\n const sha = action.data.previousSha as string;\n logger.info(` ↩️ Resetting to ${sha}`);\n await this.ops.resetHard(sha, rootPath);\n }\n } catch (error: unknown) {\n const msg = error instanceof Error ? error.message : String(error);\n logger.warn(` ⚠️ Failed to rollback git ${action.action}: ${msg}`);\n }\n }\n }\n\n private async validateTags(context: Context): Promise<void> {\n const { changedPackages, versions, rootPath, logger } = context;\n if (!versions) return;\n\n const existingTags: string[] = [];\n\n for (const pkg of changedPackages) {\n const version = versions[pkg.name];\n if (!version) continue;\n\n const tag = this.config.tagFormat.replace('{name}', pkg.name).replace('{version}', version);\n\n if (await this.ops.tagExists(tag, rootPath)) {\n existingTags.push(tag);\n }\n }\n\n if (existingTags.length > 0) {\n logger.error(`❌ Git tags already exist: ${existingTags.join(', ')}`);\n throw new Error(\n `Cannot release: git tags already exist (${existingTags.join(', ')}). Delete them first or bump to a new version.`,\n );\n }\n }\n}\n\nexport { defaultGitOperations, type GitOperations } from './operations.js';\n"],"mappings":"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"}
|