@sparkleideas/deployment 3.0.0-alpha.8

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.
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Dry Run Example
3
+ *
4
+ * Test release without making actual changes
5
+ */
6
+
7
+ import { prepareRelease, publishToNpm, validate } from '@sparkleideas/deployment';
8
+
9
+ async function dryRunRelease() {
10
+ console.log('Dry Run Mode - Testing release without changes\n');
11
+
12
+ // Validate package
13
+ console.log('1. Validating package...');
14
+ const validation = await validate();
15
+
16
+ if (!validation.valid) {
17
+ console.log(' ⚠️ Validation issues found:');
18
+ validation.errors.forEach(err => console.log(` - ${err}`));
19
+ console.log('\n (Continuing anyway in dry-run mode)\n');
20
+ } else {
21
+ console.log(' ✅ Package validation passed\n');
22
+ }
23
+
24
+ // Prepare release (dry run)
25
+ console.log('2. Preparing release (dry run)...');
26
+ const release = await prepareRelease({
27
+ bumpType: 'minor',
28
+ generateChangelog: true,
29
+ createTag: true,
30
+ commit: true,
31
+ dryRun: true // No actual changes
32
+ });
33
+
34
+ if (release.success) {
35
+ console.log(' Would create release:');
36
+ console.log(` Version: ${release.oldVersion} → ${release.newVersion}`);
37
+ console.log(` Tag: ${release.tag}`);
38
+ console.log(' \n Generated changelog:');
39
+ if (release.changelog) {
40
+ release.changelog.split('\n').forEach(line => {
41
+ console.log(` ${line}`);
42
+ });
43
+ }
44
+ console.log();
45
+ }
46
+
47
+ // Publish (dry run)
48
+ console.log('3. Publishing to npm (dry run)...');
49
+ const publish = await publishToNpm({
50
+ tag: 'latest',
51
+ access: 'public',
52
+ dryRun: true // No actual publish
53
+ });
54
+
55
+ if (publish.success) {
56
+ console.log(' Would publish:');
57
+ console.log(` Package: ${publish.packageName}@${publish.version}`);
58
+ console.log(` Tag: ${publish.tag}`);
59
+ console.log();
60
+ }
61
+
62
+ console.log('✅ Dry run completed - no changes were made');
63
+ console.log(' To perform actual release, remove dryRun: true option');
64
+ }
65
+
66
+ // Run dry run
67
+ dryRunRelease().catch(error => {
68
+ console.error('Dry run failed:', error);
69
+ process.exit(1);
70
+ });
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Prerelease Workflow Example
3
+ *
4
+ * Demonstrates publishing alpha/beta/rc versions
5
+ */
6
+
7
+ import { prepareRelease, publishToNpm } from '@sparkleideas/deployment';
8
+
9
+ async function prereleaseWorkflow() {
10
+ console.log('Prerelease Workflow\n');
11
+
12
+ // Alpha release (early development)
13
+ console.log('1. Creating alpha release...');
14
+ const alpha = await prepareRelease({
15
+ bumpType: 'prerelease',
16
+ channel: 'alpha',
17
+ generateChangelog: true,
18
+ createTag: true,
19
+ commit: true
20
+ });
21
+
22
+ if (alpha.success) {
23
+ console.log(` Created: ${alpha.newVersion}`);
24
+
25
+ await publishToNpm({
26
+ tag: 'alpha',
27
+ access: 'public'
28
+ });
29
+ console.log(' Published to npm with tag "alpha"\n');
30
+ }
31
+
32
+ // Beta release (feature complete, testing)
33
+ console.log('2. Creating beta release...');
34
+ const beta = await prepareRelease({
35
+ bumpType: 'prerelease',
36
+ channel: 'beta',
37
+ generateChangelog: true,
38
+ createTag: true,
39
+ commit: true
40
+ });
41
+
42
+ if (beta.success) {
43
+ console.log(` Created: ${beta.newVersion}`);
44
+
45
+ await publishToNpm({
46
+ tag: 'beta',
47
+ access: 'public'
48
+ });
49
+ console.log(' Published to npm with tag "beta"\n');
50
+ }
51
+
52
+ // RC release (release candidate)
53
+ console.log('3. Creating release candidate...');
54
+ const rc = await prepareRelease({
55
+ bumpType: 'prerelease',
56
+ channel: 'rc',
57
+ generateChangelog: true,
58
+ createTag: true,
59
+ commit: true
60
+ });
61
+
62
+ if (rc.success) {
63
+ console.log(` Created: ${rc.newVersion}`);
64
+
65
+ await publishToNpm({
66
+ tag: 'rc',
67
+ access: 'public'
68
+ });
69
+ console.log(' Published to npm with tag "rc"\n');
70
+ }
71
+
72
+ // Final release (stable)
73
+ console.log('4. Creating final release...');
74
+ const final = await prepareRelease({
75
+ bumpType: 'patch',
76
+ generateChangelog: true,
77
+ createTag: true,
78
+ commit: true
79
+ });
80
+
81
+ if (final.success) {
82
+ console.log(` Created: ${final.newVersion}`);
83
+
84
+ await publishToNpm({
85
+ tag: 'latest',
86
+ access: 'public'
87
+ });
88
+ console.log(' Published to npm with tag "latest"\n');
89
+ }
90
+
91
+ console.log('✅ Prerelease workflow completed!');
92
+ }
93
+
94
+ // Run the workflow
95
+ prereleaseWorkflow().catch(error => {
96
+ console.error('Workflow failed:', error);
97
+ process.exit(1);
98
+ });
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@sparkleideas/deployment",
3
+ "version": "3.0.0-alpha.8",
4
+ "type": "module",
5
+ "description": "Deployment module - Release management, CI/CD, versioning",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": "./dist/index.js"
10
+ },
11
+ "scripts": {
12
+ "build": "tsc",
13
+ "test": "vitest run",
14
+ "release": "semantic-release"
15
+ },
16
+ "dependencies": {
17
+ "@sparkleideas/shared": "^3.0.0-alpha.1"
18
+ },
19
+ "devDependencies": {
20
+ "semantic-release": "^25.0.2",
21
+ "vitest": "^4.0.16"
22
+ },
23
+ "publishConfig": {
24
+ "access": "public",
25
+ "tag": "v3alpha"
26
+ }
27
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Tests for Deployment Module
3
+ */
4
+ import { describe, it, expect } from 'vitest';
5
+ import { ReleaseManager } from '../index.js';
6
+
7
+ describe('ReleaseManager', () => {
8
+ describe('Constructor', () => {
9
+ it('should create with default cwd', () => {
10
+ const manager = new ReleaseManager();
11
+ expect(manager).toBeDefined();
12
+ });
13
+
14
+ it('should create with custom cwd', () => {
15
+ const manager = new ReleaseManager('/custom/path');
16
+ expect(manager).toBeDefined();
17
+ });
18
+ });
19
+ });
20
+
21
+ describe('Version Bump Logic', () => {
22
+ it('should parse version parts', () => {
23
+ const version = '1.2.3';
24
+ const parts = version.split('.').map(Number);
25
+
26
+ expect(parts[0]).toBe(1);
27
+ expect(parts[1]).toBe(2);
28
+ expect(parts[2]).toBe(3);
29
+ });
30
+
31
+ it('should bump patch version', () => {
32
+ const parts = '1.0.0'.split('.').map(Number);
33
+ const newVersion = [parts[0], parts[1], parts[2] + 1].join('.');
34
+ expect(newVersion).toBe('1.0.1');
35
+ });
36
+
37
+ it('should bump minor version', () => {
38
+ const parts = '1.0.0'.split('.').map(Number);
39
+ const newVersion = [parts[0], parts[1] + 1, 0].join('.');
40
+ expect(newVersion).toBe('1.1.0');
41
+ });
42
+
43
+ it('should bump major version', () => {
44
+ const parts = '1.0.0'.split('.').map(Number);
45
+ const newVersion = [parts[0] + 1, 0, 0].join('.');
46
+ expect(newVersion).toBe('2.0.0');
47
+ });
48
+ });
49
+
50
+ describe('Changelog Generation', () => {
51
+ it('should format date correctly', () => {
52
+ const date = new Date('2026-01-05');
53
+ const formatted = date.toISOString().split('T')[0];
54
+ expect(formatted).toBe('2026-01-05');
55
+ });
56
+
57
+ it('should categorize commits by type', () => {
58
+ const commits = [
59
+ { message: 'feat: add new feature' },
60
+ { message: 'fix: fix bug' },
61
+ { message: 'docs: update docs' },
62
+ ];
63
+
64
+ const feat = commits.filter(c => c.message.startsWith('feat:'));
65
+ const fix = commits.filter(c => c.message.startsWith('fix:'));
66
+ const docs = commits.filter(c => c.message.startsWith('docs:'));
67
+
68
+ expect(feat).toHaveLength(1);
69
+ expect(fix).toHaveLength(1);
70
+ expect(docs).toHaveLength(1);
71
+ });
72
+ });
package/src/index.ts ADDED
@@ -0,0 +1,88 @@
1
+ /**
2
+ * @sparkleideas/deployment
3
+ * Release management, CI/CD, and versioning module
4
+ */
5
+
6
+ // Export types
7
+ export type {
8
+ VersionBumpType,
9
+ ReleaseChannel,
10
+ ReleaseOptions,
11
+ ReleaseResult,
12
+ PublishOptions,
13
+ PublishResult,
14
+ ValidationOptions,
15
+ ValidationResult,
16
+ PackageInfo,
17
+ GitCommit,
18
+ ChangelogEntry
19
+ } from './types.js';
20
+
21
+ // Export classes
22
+ export { ReleaseManager } from './release-manager.js';
23
+ export { Publisher } from './publisher.js';
24
+ export { Validator } from './validator.js';
25
+
26
+ // Export convenience functions
27
+ export {
28
+ prepareRelease
29
+ } from './release-manager.js';
30
+
31
+ export {
32
+ publishToNpm,
33
+ checkVersionExists,
34
+ getLatestVersion
35
+ } from './publisher.js';
36
+
37
+ export {
38
+ validate
39
+ } from './validator.js';
40
+
41
+ // Legacy exports for backward compatibility
42
+ export interface ReleaseConfig {
43
+ version: string;
44
+ channel: 'alpha' | 'beta' | 'stable';
45
+ changelog: boolean;
46
+ dryRun: boolean;
47
+ }
48
+
49
+ export interface DeploymentTarget {
50
+ name: string;
51
+ type: 'npm' | 'docker' | 'github-release';
52
+ config: Record<string, unknown>;
53
+ }
54
+
55
+ /**
56
+ * Legacy prepare release function
57
+ * @deprecated Use prepareRelease from release-manager instead
58
+ */
59
+ export async function prepare(config: ReleaseConfig): Promise<void> {
60
+ const { ReleaseManager } = await import('./release-manager.js');
61
+ const manager = new ReleaseManager();
62
+
63
+ await manager.prepareRelease({
64
+ version: config.version,
65
+ channel: config.channel as any,
66
+ generateChangelog: config.changelog,
67
+ dryRun: config.dryRun
68
+ });
69
+ }
70
+
71
+ /**
72
+ * Legacy deploy function
73
+ * @deprecated Use publishToNpm from publisher instead
74
+ */
75
+ export async function deploy(target: DeploymentTarget): Promise<void> {
76
+ if (target.type === 'npm') {
77
+ const { Publisher } = await import('./publisher.js');
78
+ const publisher = new Publisher();
79
+
80
+ await publisher.publishToNpm({
81
+ tag: (target.config.tag as string) || 'latest',
82
+ dryRun: (target.config.dryRun as boolean) || false
83
+ });
84
+ } else {
85
+ console.log(`Deploying to ${target.name} (${target.type})`);
86
+ throw new Error(`Deployment type ${target.type} not yet implemented`);
87
+ }
88
+ }
@@ -0,0 +1,273 @@
1
+ /**
2
+ * NPM Publisher
3
+ * Handles npm package publishing with tag support
4
+ */
5
+
6
+ import { execSync, execFileSync } from 'child_process';
7
+ import { readFileSync, existsSync } from 'fs';
8
+ import { join } from 'path';
9
+ import type { PublishOptions, PublishResult, PackageInfo } from './types.js';
10
+
11
+ export class Publisher {
12
+ private cwd: string;
13
+
14
+ constructor(cwd: string = process.cwd()) {
15
+ this.cwd = cwd;
16
+ }
17
+
18
+ /**
19
+ * Publish package to npm
20
+ */
21
+ async publishToNpm(options: PublishOptions = {}): Promise<PublishResult> {
22
+ const {
23
+ tag = 'latest',
24
+ access,
25
+ dryRun = false,
26
+ registry,
27
+ otp,
28
+ skipBuild = false,
29
+ buildCommand = 'npm run build'
30
+ } = options;
31
+
32
+ const result: PublishResult = {
33
+ packageName: '',
34
+ version: '',
35
+ tag,
36
+ success: false
37
+ };
38
+
39
+ try {
40
+ // Read package.json
41
+ const pkgPath = join(this.cwd, 'package.json');
42
+ if (!existsSync(pkgPath)) {
43
+ throw new Error('package.json not found');
44
+ }
45
+
46
+ const pkg: PackageInfo = JSON.parse(readFileSync(pkgPath, 'utf-8'));
47
+
48
+ if (pkg.private) {
49
+ throw new Error('Cannot publish private package');
50
+ }
51
+
52
+ result.packageName = pkg.name;
53
+ result.version = pkg.version;
54
+
55
+ // Run build if not skipped
56
+ if (!skipBuild) {
57
+ console.log('Building package...');
58
+ this.execCommand(buildCommand);
59
+ }
60
+
61
+ // Construct npm publish command arguments (without 'npm' prefix for execNpmCommand)
62
+ const publishArgs: string[] = ['publish'];
63
+
64
+ if (tag) {
65
+ publishArgs.push('--tag', tag);
66
+ }
67
+
68
+ if (access) {
69
+ publishArgs.push('--access', access);
70
+ }
71
+
72
+ if (registry) {
73
+ publishArgs.push('--registry', registry);
74
+ }
75
+
76
+ if (otp) {
77
+ publishArgs.push('--otp', otp);
78
+ }
79
+
80
+ if (dryRun) {
81
+ publishArgs.push('--dry-run');
82
+ }
83
+
84
+ // Execute publish
85
+ console.log(`Publishing ${result.packageName}@${result.version} with tag '${tag}'...`);
86
+
87
+ if (dryRun) {
88
+ console.log('Dry run mode - no actual publish');
89
+ console.log('Command: npm', publishArgs.join(' '));
90
+ }
91
+
92
+ const output = this.execNpmCommand(publishArgs, true);
93
+
94
+ // Parse output for tarball URL
95
+ const tarballMatch = output.match(/https:\/\/[^\s]+\.tgz/);
96
+ if (tarballMatch) {
97
+ result.tarball = tarballMatch[0];
98
+ }
99
+
100
+ result.publishedAt = new Date();
101
+ result.success = true;
102
+
103
+ console.log(`Successfully published ${result.packageName}@${result.version}`);
104
+
105
+ return result;
106
+
107
+ } catch (error) {
108
+ result.error = error instanceof Error ? error.message : String(error);
109
+ console.error('Publish failed:', result.error);
110
+ return result;
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Check if package version already exists on npm
116
+ */
117
+ async checkVersionExists(packageName: string, version: string): Promise<boolean> {
118
+ try {
119
+ const output = this.execNpmCommand(['view', `${packageName}@${version}`, 'version'], true);
120
+ return output.trim() === version;
121
+ } catch {
122
+ return false;
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Get latest published version
128
+ */
129
+ async getLatestVersion(packageName: string, tag = 'latest'): Promise<string | null> {
130
+ try {
131
+ const output = this.execNpmCommand(['view', `${packageName}@${tag}`, 'version'], true);
132
+ return output.trim();
133
+ } catch {
134
+ return null;
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Get package info from npm registry
140
+ */
141
+ async getPackageInfo(packageName: string): Promise<PackageInfo | null> {
142
+ try {
143
+ const output = this.execNpmCommand(['view', packageName, '--json'], true);
144
+ return JSON.parse(output);
145
+ } catch {
146
+ return null;
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Verify npm authentication
152
+ */
153
+ async verifyAuth(): Promise<boolean> {
154
+ try {
155
+ const output = this.execNpmCommand(['whoami'], true);
156
+ return output.trim().length > 0;
157
+ } catch {
158
+ return false;
159
+ }
160
+ }
161
+
162
+ /**
163
+ * Get npm registry URL
164
+ */
165
+ async getRegistry(): Promise<string> {
166
+ try {
167
+ const output = this.execNpmCommand(['config', 'get', 'registry'], true);
168
+ return output.trim();
169
+ } catch {
170
+ return 'https://registry.npmjs.org/';
171
+ }
172
+ }
173
+
174
+ /**
175
+ * Pack package to tarball
176
+ */
177
+ async pack(outputDir?: string): Promise<string> {
178
+ try {
179
+ const packArgs = ['pack'];
180
+ if (outputDir) {
181
+ packArgs.push('--pack-destination', outputDir);
182
+ }
183
+
184
+ const output = this.execNpmCommand(packArgs, true);
185
+ const tarballName = output.trim().split('\n').pop() || '';
186
+
187
+ return outputDir ? join(outputDir, tarballName) : tarballName;
188
+ } catch (error) {
189
+ throw new Error(`Failed to pack: ${error}`);
190
+ }
191
+ }
192
+
193
+ /**
194
+ * Execute npm command safely using execFileSync
195
+ */
196
+ private execNpmCommand(args: string[], returnOutput = false): string {
197
+ try {
198
+ // Validate args don't contain shell metacharacters
199
+ for (const arg of args) {
200
+ if (/[;&|`$()<>]/.test(arg)) {
201
+ throw new Error(`Invalid argument: contains shell metacharacters`);
202
+ }
203
+ }
204
+ const output = execFileSync('npm', args, {
205
+ cwd: this.cwd,
206
+ encoding: 'utf-8',
207
+ shell: false,
208
+ stdio: returnOutput ? ['pipe', 'pipe', 'pipe'] : 'inherit'
209
+ });
210
+ return returnOutput ? output : '';
211
+ } catch (error) {
212
+ throw error;
213
+ }
214
+ }
215
+
216
+ /**
217
+ * Execute command (for build scripts only - validated)
218
+ */
219
+ private execCommand(cmd: string, returnOutput = false): string {
220
+ // Only allow npm/npx build commands for safety
221
+ const allowedPrefixes = ['npm run ', 'npm ', 'npx ', 'pnpm ', 'yarn '];
222
+ const isAllowed = allowedPrefixes.some(prefix => cmd.startsWith(prefix));
223
+ if (!isAllowed) {
224
+ throw new Error(`Disallowed command: only npm/npx/pnpm/yarn commands are permitted`);
225
+ }
226
+ // Validate no dangerous shell metacharacters
227
+ if (/[;&|`$()<>]/.test(cmd)) {
228
+ throw new Error(`Invalid command: contains shell metacharacters`);
229
+ }
230
+ try {
231
+ const output = execSync(cmd, {
232
+ cwd: this.cwd,
233
+ encoding: 'utf-8',
234
+ stdio: returnOutput ? 'pipe' : 'inherit'
235
+ });
236
+ return returnOutput ? output : '';
237
+ } catch (error) {
238
+ throw error;
239
+ }
240
+ }
241
+ }
242
+
243
+ /**
244
+ * Convenience function to publish to npm
245
+ */
246
+ export async function publishToNpm(
247
+ options: PublishOptions = {}
248
+ ): Promise<PublishResult> {
249
+ const publisher = new Publisher();
250
+ return publisher.publishToNpm(options);
251
+ }
252
+
253
+ /**
254
+ * Convenience function to check version exists
255
+ */
256
+ export async function checkVersionExists(
257
+ packageName: string,
258
+ version: string
259
+ ): Promise<boolean> {
260
+ const publisher = new Publisher();
261
+ return publisher.checkVersionExists(packageName, version);
262
+ }
263
+
264
+ /**
265
+ * Convenience function to get latest version
266
+ */
267
+ export async function getLatestVersion(
268
+ packageName: string,
269
+ tag?: string
270
+ ): Promise<string | null> {
271
+ const publisher = new Publisher();
272
+ return publisher.getLatestVersion(packageName, tag);
273
+ }