@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.
- package/QUICK_START.md +281 -0
- package/README.md +333 -0
- package/examples/basic-release.ts +92 -0
- package/examples/dry-run.ts +70 -0
- package/examples/prerelease-workflow.ts +98 -0
- package/package.json +27 -0
- package/src/__tests__/release-manager.test.ts +72 -0
- package/src/index.ts +88 -0
- package/src/publisher.ts +273 -0
- package/src/release-manager.ts +380 -0
- package/src/types.ts +159 -0
- package/src/validator.ts +310 -0
- package/tmp.json +0 -0
- package/tsconfig.json +9 -0
package/src/validator.ts
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pre-Release Validator
|
|
3
|
+
* Validates package before release (lint, test, build, dependencies)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { execSync } from 'child_process';
|
|
7
|
+
import { readFileSync, existsSync } from 'fs';
|
|
8
|
+
import { join } from 'path';
|
|
9
|
+
import type { ValidationOptions, ValidationResult, PackageInfo } from './types.js';
|
|
10
|
+
|
|
11
|
+
export class Validator {
|
|
12
|
+
private cwd: string;
|
|
13
|
+
|
|
14
|
+
constructor(cwd: string = process.cwd()) {
|
|
15
|
+
this.cwd = cwd;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Run all validation checks
|
|
20
|
+
*/
|
|
21
|
+
async validate(options: ValidationOptions = {}): Promise<ValidationResult> {
|
|
22
|
+
const {
|
|
23
|
+
lint = true,
|
|
24
|
+
test = true,
|
|
25
|
+
build = true,
|
|
26
|
+
checkDependencies = true,
|
|
27
|
+
checkGitStatus = true,
|
|
28
|
+
lintCommand = 'npm run lint',
|
|
29
|
+
testCommand = 'npm test',
|
|
30
|
+
buildCommand = 'npm run build'
|
|
31
|
+
} = options;
|
|
32
|
+
|
|
33
|
+
const result: ValidationResult = {
|
|
34
|
+
valid: true,
|
|
35
|
+
checks: {},
|
|
36
|
+
errors: [],
|
|
37
|
+
warnings: []
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// Validate package.json
|
|
41
|
+
console.log('Validating package.json...');
|
|
42
|
+
result.checks.packageJson = await this.validatePackageJson();
|
|
43
|
+
if (!result.checks.packageJson.passed) {
|
|
44
|
+
result.valid = false;
|
|
45
|
+
result.errors.push(...(result.checks.packageJson.errors || []));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Check git status
|
|
49
|
+
if (checkGitStatus) {
|
|
50
|
+
console.log('Checking git status...');
|
|
51
|
+
result.checks.gitStatus = await this.checkGitStatus();
|
|
52
|
+
if (!result.checks.gitStatus.passed) {
|
|
53
|
+
result.warnings.push(...(result.checks.gitStatus.errors || []));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Check dependencies
|
|
58
|
+
if (checkDependencies) {
|
|
59
|
+
console.log('Checking dependencies...');
|
|
60
|
+
result.checks.dependencies = await this.checkDependencies();
|
|
61
|
+
if (!result.checks.dependencies.passed) {
|
|
62
|
+
result.valid = false;
|
|
63
|
+
result.errors.push(...(result.checks.dependencies.errors || []));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Run linter
|
|
68
|
+
if (lint) {
|
|
69
|
+
console.log('Running linter...');
|
|
70
|
+
result.checks.lint = await this.runLint(lintCommand);
|
|
71
|
+
if (!result.checks.lint.passed) {
|
|
72
|
+
result.valid = false;
|
|
73
|
+
result.errors.push(...(result.checks.lint.errors || []));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Run tests
|
|
78
|
+
if (test) {
|
|
79
|
+
console.log('Running tests...');
|
|
80
|
+
result.checks.test = await this.runTests(testCommand);
|
|
81
|
+
if (!result.checks.test.passed) {
|
|
82
|
+
result.valid = false;
|
|
83
|
+
result.errors.push(...(result.checks.test.errors || []));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Run build
|
|
88
|
+
if (build) {
|
|
89
|
+
console.log('Running build...');
|
|
90
|
+
result.checks.build = await this.runBuild(buildCommand);
|
|
91
|
+
if (!result.checks.build.passed) {
|
|
92
|
+
result.valid = false;
|
|
93
|
+
result.errors.push(...(result.checks.build.errors || []));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Validate package.json structure and required fields
|
|
102
|
+
*/
|
|
103
|
+
private async validatePackageJson(): Promise<{ passed: boolean; errors?: string[] }> {
|
|
104
|
+
const errors: string[] = [];
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
const pkgPath = join(this.cwd, 'package.json');
|
|
108
|
+
if (!existsSync(pkgPath)) {
|
|
109
|
+
errors.push('package.json not found');
|
|
110
|
+
return { passed: false, errors };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const pkg: PackageInfo = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
114
|
+
|
|
115
|
+
// Check required fields
|
|
116
|
+
if (!pkg.name) {
|
|
117
|
+
errors.push('package.json missing "name" field');
|
|
118
|
+
}
|
|
119
|
+
if (!pkg.version) {
|
|
120
|
+
errors.push('package.json missing "version" field');
|
|
121
|
+
}
|
|
122
|
+
if (!pkg.description) {
|
|
123
|
+
errors.push('package.json missing "description" field (warning)');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Validate version format
|
|
127
|
+
if (pkg.version && !/^\d+\.\d+\.\d+(?:-[a-z]+\.\d+)?$/.test(pkg.version)) {
|
|
128
|
+
errors.push(`Invalid version format: ${pkg.version}`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Check for repository field
|
|
132
|
+
if (!pkg.repository) {
|
|
133
|
+
errors.push('package.json missing "repository" field (recommended)');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Warn about private package
|
|
137
|
+
if (pkg.private) {
|
|
138
|
+
errors.push('Package is marked as private - cannot be published');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
passed: errors.length === 0,
|
|
143
|
+
errors: errors.length > 0 ? errors : undefined
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
} catch (error) {
|
|
147
|
+
errors.push(`Failed to parse package.json: ${error}`);
|
|
148
|
+
return { passed: false, errors };
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Check git status for uncommitted changes
|
|
154
|
+
*/
|
|
155
|
+
private async checkGitStatus(): Promise<{ passed: boolean; errors?: string[] }> {
|
|
156
|
+
try {
|
|
157
|
+
const status = this.execCommand('git status --porcelain', true);
|
|
158
|
+
|
|
159
|
+
if (status.trim()) {
|
|
160
|
+
return {
|
|
161
|
+
passed: false,
|
|
162
|
+
errors: ['Uncommitted changes detected. Commit or stash changes before release.']
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return { passed: true };
|
|
167
|
+
} catch (error) {
|
|
168
|
+
return {
|
|
169
|
+
passed: false,
|
|
170
|
+
errors: [`Git status check failed: ${error}`]
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Check for dependency issues
|
|
177
|
+
*/
|
|
178
|
+
private async checkDependencies(): Promise<{ passed: boolean; errors?: string[] }> {
|
|
179
|
+
const errors: string[] = [];
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
// Check for npm audit issues
|
|
183
|
+
try {
|
|
184
|
+
this.execCommand('npm audit --audit-level moderate', false);
|
|
185
|
+
} catch (error) {
|
|
186
|
+
errors.push('npm audit found security vulnerabilities');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Check for outdated dependencies (non-critical)
|
|
190
|
+
try {
|
|
191
|
+
const outdated = this.execCommand('npm outdated --json', true);
|
|
192
|
+
if (outdated.trim()) {
|
|
193
|
+
const outdatedPkgs = JSON.parse(outdated);
|
|
194
|
+
const count = Object.keys(outdatedPkgs).length;
|
|
195
|
+
if (count > 0) {
|
|
196
|
+
errors.push(`${count} outdated dependencies found (warning)`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
} catch {
|
|
200
|
+
// Outdated check is non-critical
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
passed: errors.length === 0,
|
|
205
|
+
errors: errors.length > 0 ? errors : undefined
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
} catch (error) {
|
|
209
|
+
errors.push(`Dependency check failed: ${error}`);
|
|
210
|
+
return { passed: false, errors };
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Run linter
|
|
216
|
+
*/
|
|
217
|
+
private async runLint(command: string): Promise<{ passed: boolean; errors?: string[] }> {
|
|
218
|
+
try {
|
|
219
|
+
this.execCommand(command, false);
|
|
220
|
+
return { passed: true };
|
|
221
|
+
} catch (error) {
|
|
222
|
+
return {
|
|
223
|
+
passed: false,
|
|
224
|
+
errors: [`Linting failed: ${error}`]
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Run tests
|
|
231
|
+
*/
|
|
232
|
+
private async runTests(command: string): Promise<{ passed: boolean; errors?: string[] }> {
|
|
233
|
+
try {
|
|
234
|
+
this.execCommand(command, false);
|
|
235
|
+
return { passed: true };
|
|
236
|
+
} catch (error) {
|
|
237
|
+
return {
|
|
238
|
+
passed: false,
|
|
239
|
+
errors: [`Tests failed: ${error}`]
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Run build
|
|
246
|
+
*/
|
|
247
|
+
private async runBuild(command: string): Promise<{ passed: boolean; errors?: string[] }> {
|
|
248
|
+
try {
|
|
249
|
+
this.execCommand(command, false);
|
|
250
|
+
return { passed: true };
|
|
251
|
+
} catch (error) {
|
|
252
|
+
return {
|
|
253
|
+
passed: false,
|
|
254
|
+
errors: [`Build failed: ${error}`]
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Allowed commands for security validation
|
|
261
|
+
*/
|
|
262
|
+
private static readonly ALLOWED_COMMAND_PREFIXES = [
|
|
263
|
+
'npm run ',
|
|
264
|
+
'npm ',
|
|
265
|
+
'npx ',
|
|
266
|
+
'git ',
|
|
267
|
+
];
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Execute command safely with validation
|
|
271
|
+
*/
|
|
272
|
+
private execCommand(cmd: string, returnOutput = false): string {
|
|
273
|
+
// Validate: check for shell metacharacters
|
|
274
|
+
if (/[;&|`$()<>]/.test(cmd)) {
|
|
275
|
+
throw new Error(`Invalid command: contains shell metacharacters`);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Validate: must start with allowed prefix
|
|
279
|
+
const isAllowed = Validator.ALLOWED_COMMAND_PREFIXES.some(
|
|
280
|
+
prefix => cmd.startsWith(prefix)
|
|
281
|
+
);
|
|
282
|
+
if (!isAllowed) {
|
|
283
|
+
throw new Error(`Command not allowed: ${cmd.split(' ')[0]}`);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
try {
|
|
287
|
+
const output = execSync(cmd, {
|
|
288
|
+
cwd: this.cwd,
|
|
289
|
+
encoding: 'utf-8',
|
|
290
|
+
stdio: returnOutput ? 'pipe' : 'inherit',
|
|
291
|
+
timeout: 60000, // 60 second timeout for builds
|
|
292
|
+
maxBuffer: 50 * 1024 * 1024, // 50MB buffer for build output
|
|
293
|
+
});
|
|
294
|
+
return returnOutput ? output : '';
|
|
295
|
+
} catch (error) {
|
|
296
|
+
if (returnOutput && error instanceof Error) {
|
|
297
|
+
throw error;
|
|
298
|
+
}
|
|
299
|
+
throw error;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Convenience function to validate package
|
|
306
|
+
*/
|
|
307
|
+
export async function validate(options: ValidationOptions = {}): Promise<ValidationResult> {
|
|
308
|
+
const validator = new Validator();
|
|
309
|
+
return validator.validate(options);
|
|
310
|
+
}
|
package/tmp.json
ADDED
|
File without changes
|