pgpm 1.1.5 → 1.2.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/README.md +41 -0
- package/commands/test-packages.d.ts +4 -0
- package/commands/test-packages.js +320 -0
- package/commands.js +2 -0
- package/esm/commands/test-packages.js +315 -0
- package/esm/commands.js +2 -0
- package/esm/index.js +1 -0
- package/index.d.ts +1 -0
- package/index.js +3 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -90,6 +90,10 @@ Here are some useful commands for reference:
|
|
|
90
90
|
- `pgpm plan` - Generate deployment plans for your modules
|
|
91
91
|
- `pgpm package` - Package your module for distribution
|
|
92
92
|
|
|
93
|
+
### Testing
|
|
94
|
+
|
|
95
|
+
- `pgpm test-packages` - Run integration tests on all modules in a workspace
|
|
96
|
+
|
|
93
97
|
### Utilities
|
|
94
98
|
|
|
95
99
|
- `pgpm add` - Add a new database change
|
|
@@ -302,6 +306,43 @@ pgpm kill
|
|
|
302
306
|
pgpm kill --no-drop
|
|
303
307
|
```
|
|
304
308
|
|
|
309
|
+
### Testing
|
|
310
|
+
|
|
311
|
+
#### `pgpm test-packages`
|
|
312
|
+
|
|
313
|
+
Run integration tests on all modules in a workspace. Creates a temporary database for each module, deploys, and optionally runs verify/revert/deploy cycles.
|
|
314
|
+
|
|
315
|
+
```bash
|
|
316
|
+
# Test all modules in workspace (deploy only)
|
|
317
|
+
pgpm test-packages
|
|
318
|
+
|
|
319
|
+
# Run full deploy/verify/revert/deploy cycle
|
|
320
|
+
pgpm test-packages --full-cycle
|
|
321
|
+
|
|
322
|
+
# Stop on first failure
|
|
323
|
+
pgpm test-packages --stop-on-fail
|
|
324
|
+
|
|
325
|
+
# Exclude specific modules
|
|
326
|
+
pgpm test-packages --exclude my-module,another-module
|
|
327
|
+
|
|
328
|
+
# Combine options
|
|
329
|
+
pgpm test-packages --full-cycle --stop-on-fail --exclude legacy-module
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
**Options:**
|
|
333
|
+
|
|
334
|
+
- `--full-cycle` - Run full deploy/verify/revert/deploy cycle (default: deploy only)
|
|
335
|
+
- `--stop-on-fail` - Stop testing immediately when a module fails
|
|
336
|
+
- `--exclude <modules>` - Comma-separated module names to exclude
|
|
337
|
+
- `--cwd <directory>` - Working directory (default: current directory)
|
|
338
|
+
|
|
339
|
+
**Notes:**
|
|
340
|
+
|
|
341
|
+
- Discovers modules from workspace `pgpm.json` configuration
|
|
342
|
+
- Creates isolated test databases (`test_<module_name>`) for each module
|
|
343
|
+
- Automatically cleans up test databases after each test
|
|
344
|
+
- Uses internal APIs for deploy/verify/revert operations
|
|
345
|
+
|
|
305
346
|
## ⚙️ Configuration
|
|
306
347
|
|
|
307
348
|
### Environment Variables
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const core_1 = require("@pgpmjs/core");
|
|
7
|
+
const env_1 = require("@pgpmjs/env");
|
|
8
|
+
const logger_1 = require("@pgpmjs/logger");
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const pg_env_1 = require("pg-env");
|
|
11
|
+
const pg_cache_1 = require("pg-cache");
|
|
12
|
+
const log = new logger_1.Logger('test-packages');
|
|
13
|
+
// ANSI color codes
|
|
14
|
+
const RED = '\x1b[0;31m';
|
|
15
|
+
const GREEN = '\x1b[0;32m';
|
|
16
|
+
const YELLOW = '\x1b[1;33m';
|
|
17
|
+
const NC = '\x1b[0m'; // No Color
|
|
18
|
+
const testPackagesUsageText = `
|
|
19
|
+
Test Packages Command:
|
|
20
|
+
|
|
21
|
+
pgpm test-packages [OPTIONS]
|
|
22
|
+
|
|
23
|
+
Run integration tests on all PGPM packages in a workspace.
|
|
24
|
+
Tests each package with a deploy/verify/revert/deploy cycle.
|
|
25
|
+
|
|
26
|
+
Options:
|
|
27
|
+
--help, -h Show this help message
|
|
28
|
+
--exclude <pkgs> Comma-separated module names to exclude
|
|
29
|
+
--stop-on-fail Stop testing immediately when a package fails
|
|
30
|
+
--full-cycle Run full deploy/verify/revert/deploy cycle (default: deploy only)
|
|
31
|
+
--cwd <directory> Working directory (default: current directory)
|
|
32
|
+
|
|
33
|
+
Examples:
|
|
34
|
+
pgpm test-packages Test all packages in workspace
|
|
35
|
+
pgpm test-packages --full-cycle Run full test cycle with verify/revert
|
|
36
|
+
pgpm test-packages --stop-on-fail Stop on first failure
|
|
37
|
+
pgpm test-packages --exclude my-module Exclude specific modules
|
|
38
|
+
`;
|
|
39
|
+
function dbSafeName(moduleName) {
|
|
40
|
+
return `test_${moduleName}`.replace(/[^a-zA-Z0-9]/g, '_').toLowerCase();
|
|
41
|
+
}
|
|
42
|
+
async function createDatabase(dbname, adminDb = 'postgres') {
|
|
43
|
+
try {
|
|
44
|
+
const pgEnv = (0, pg_env_1.getPgEnvOptions)({ database: adminDb });
|
|
45
|
+
const pool = (0, pg_cache_1.getPgPool)(pgEnv);
|
|
46
|
+
// Sanitize database name (only allow alphanumeric and underscore)
|
|
47
|
+
const safeName = dbname.replace(/[^a-zA-Z0-9_]/g, '_');
|
|
48
|
+
await pool.query(`CREATE DATABASE "${safeName}"`);
|
|
49
|
+
log.debug(`Created database: ${safeName}`);
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
if (error.code === '42P04') {
|
|
54
|
+
// Database already exists, that's fine
|
|
55
|
+
log.debug(`Database ${dbname} already exists`);
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
log.error(`Failed to create database ${dbname}: ${error.message}`);
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
async function dropDatabase(dbname, adminDb = 'postgres') {
|
|
63
|
+
try {
|
|
64
|
+
const pgEnv = (0, pg_env_1.getPgEnvOptions)({ database: adminDb });
|
|
65
|
+
const pool = (0, pg_cache_1.getPgPool)(pgEnv);
|
|
66
|
+
// Sanitize database name
|
|
67
|
+
const safeName = dbname.replace(/[^a-zA-Z0-9_]/g, '_');
|
|
68
|
+
// Terminate all connections to the database first
|
|
69
|
+
await pool.query(`
|
|
70
|
+
SELECT pg_terminate_backend(pid)
|
|
71
|
+
FROM pg_stat_activity
|
|
72
|
+
WHERE datname = $1 AND pid <> pg_backend_pid()
|
|
73
|
+
`, [safeName]);
|
|
74
|
+
await pool.query(`DROP DATABASE IF EXISTS "${safeName}"`);
|
|
75
|
+
log.debug(`Dropped database: ${safeName}`);
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
// Ignore errors when dropping (database might not exist)
|
|
79
|
+
log.debug(`Could not drop database ${dbname}: ${error.message}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
async function checkPostgresConnection() {
|
|
83
|
+
try {
|
|
84
|
+
const pgEnv = (0, pg_env_1.getPgEnvOptions)({ database: 'postgres' });
|
|
85
|
+
const pool = (0, pg_cache_1.getPgPool)(pgEnv);
|
|
86
|
+
await pool.query('SELECT 1');
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
async function testModule(workspacePkg, modulePkg, fullCycle) {
|
|
94
|
+
const moduleName = modulePkg.getModuleName();
|
|
95
|
+
const modulePath = modulePkg.getModulePath() || '';
|
|
96
|
+
const dbname = dbSafeName(moduleName);
|
|
97
|
+
console.log(`${YELLOW}Testing module: ${moduleName}${NC}`);
|
|
98
|
+
console.log(` Module path: ${modulePath}`);
|
|
99
|
+
console.log(` Database name: ${dbname}`);
|
|
100
|
+
// Clean up any existing test database
|
|
101
|
+
await dropDatabase(dbname);
|
|
102
|
+
// Create fresh test database
|
|
103
|
+
console.log(` Creating database: ${dbname}`);
|
|
104
|
+
if (!await createDatabase(dbname)) {
|
|
105
|
+
return {
|
|
106
|
+
moduleName,
|
|
107
|
+
modulePath,
|
|
108
|
+
success: false,
|
|
109
|
+
error: `Could not create database ${dbname}`
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
// Create options for this test database
|
|
114
|
+
const opts = (0, env_1.getEnvOptions)({
|
|
115
|
+
pg: (0, pg_env_1.getPgEnvOptions)({ database: dbname }),
|
|
116
|
+
deployment: {
|
|
117
|
+
useTx: true,
|
|
118
|
+
fast: false,
|
|
119
|
+
usePlan: true,
|
|
120
|
+
cache: false,
|
|
121
|
+
logOnly: false
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
// Deploy
|
|
125
|
+
console.log(' Running deploy...');
|
|
126
|
+
try {
|
|
127
|
+
await workspacePkg.deploy(opts, moduleName, true);
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
await dropDatabase(dbname);
|
|
131
|
+
return {
|
|
132
|
+
moduleName,
|
|
133
|
+
modulePath,
|
|
134
|
+
success: false,
|
|
135
|
+
error: `Deploy failed: ${error.message}`
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
if (fullCycle) {
|
|
139
|
+
// Verify
|
|
140
|
+
console.log(' Running verify...');
|
|
141
|
+
try {
|
|
142
|
+
await workspacePkg.verify(opts, moduleName, true);
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
await dropDatabase(dbname);
|
|
146
|
+
return {
|
|
147
|
+
moduleName,
|
|
148
|
+
modulePath,
|
|
149
|
+
success: false,
|
|
150
|
+
error: `Verify failed: ${error.message}`
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
// Revert
|
|
154
|
+
console.log(' Running revert...');
|
|
155
|
+
try {
|
|
156
|
+
await workspacePkg.revert(opts, moduleName, true);
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
await dropDatabase(dbname);
|
|
160
|
+
return {
|
|
161
|
+
moduleName,
|
|
162
|
+
modulePath,
|
|
163
|
+
success: false,
|
|
164
|
+
error: `Revert failed: ${error.message}`
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
// Deploy again
|
|
168
|
+
console.log(' Running deploy (second time)...');
|
|
169
|
+
try {
|
|
170
|
+
await workspacePkg.deploy(opts, moduleName, true);
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
await dropDatabase(dbname);
|
|
174
|
+
return {
|
|
175
|
+
moduleName,
|
|
176
|
+
modulePath,
|
|
177
|
+
success: false,
|
|
178
|
+
error: `Deploy (second time) failed: ${error.message}`
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
// Clean up test database
|
|
183
|
+
await dropDatabase(dbname);
|
|
184
|
+
console.log(`${GREEN}SUCCESS: Module ${moduleName} passed all tests${NC}`);
|
|
185
|
+
return {
|
|
186
|
+
moduleName,
|
|
187
|
+
modulePath,
|
|
188
|
+
success: true
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
// Ensure cleanup on any unexpected error
|
|
193
|
+
await dropDatabase(dbname);
|
|
194
|
+
return {
|
|
195
|
+
moduleName,
|
|
196
|
+
modulePath,
|
|
197
|
+
success: false,
|
|
198
|
+
error: `Unexpected error: ${error.message}`
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
exports.default = async (argv, _prompter, _options) => {
|
|
203
|
+
// Show usage if explicitly requested
|
|
204
|
+
if (argv.help || argv.h) {
|
|
205
|
+
console.log(testPackagesUsageText);
|
|
206
|
+
process.exit(0);
|
|
207
|
+
}
|
|
208
|
+
// Parse options
|
|
209
|
+
const stopOnFail = argv['stop-on-fail'] === true || argv.stopOnFail === true;
|
|
210
|
+
const fullCycle = argv['full-cycle'] === true || argv.fullCycle === true;
|
|
211
|
+
const cwd = argv.cwd || process.cwd();
|
|
212
|
+
// Parse excludes
|
|
213
|
+
let excludes = [];
|
|
214
|
+
if (argv.exclude) {
|
|
215
|
+
excludes = argv.exclude.split(',').map(e => e.trim());
|
|
216
|
+
}
|
|
217
|
+
console.log('=== PGPM Package Integration Test ===');
|
|
218
|
+
console.log(`Testing all packages with ${fullCycle ? 'deploy/verify/revert/deploy cycle' : 'deploy only'}`);
|
|
219
|
+
if (stopOnFail) {
|
|
220
|
+
console.log('Mode: Stop on first failure');
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
console.log('Mode: Test all packages (collect all failures)');
|
|
224
|
+
}
|
|
225
|
+
console.log('');
|
|
226
|
+
// Check PostgreSQL connection
|
|
227
|
+
console.log('Checking PostgreSQL connection...');
|
|
228
|
+
if (!await checkPostgresConnection()) {
|
|
229
|
+
log.error('PostgreSQL not accessible.');
|
|
230
|
+
console.log('Ensure PostgreSQL is running and connection settings are correct.');
|
|
231
|
+
console.log('For local development: docker-compose up -d');
|
|
232
|
+
console.log('For CI: ensure PostgreSQL service is running');
|
|
233
|
+
process.exit(1);
|
|
234
|
+
}
|
|
235
|
+
console.log('PostgreSQL connection successful');
|
|
236
|
+
console.log('');
|
|
237
|
+
// Initialize workspace package
|
|
238
|
+
const projectRoot = path_1.default.resolve(cwd);
|
|
239
|
+
const workspacePkg = new core_1.PgpmPackage(projectRoot);
|
|
240
|
+
if (!workspacePkg.getWorkspacePath()) {
|
|
241
|
+
log.error('Not in a PGPM workspace. Run this command from a workspace root.');
|
|
242
|
+
process.exit(1);
|
|
243
|
+
}
|
|
244
|
+
// Get all modules from workspace using internal API
|
|
245
|
+
console.log('Finding all PGPM modules in workspace...');
|
|
246
|
+
const modules = await workspacePkg.getModules();
|
|
247
|
+
if (modules.length === 0) {
|
|
248
|
+
log.warn('No modules found in workspace.');
|
|
249
|
+
process.exit(0);
|
|
250
|
+
}
|
|
251
|
+
// Filter out excluded modules
|
|
252
|
+
let filteredModules = modules;
|
|
253
|
+
if (excludes.length > 0) {
|
|
254
|
+
filteredModules = modules.filter(mod => {
|
|
255
|
+
const moduleName = mod.getModuleName();
|
|
256
|
+
return !excludes.includes(moduleName);
|
|
257
|
+
});
|
|
258
|
+
console.log(`Excluding: ${excludes.join(', ')}`);
|
|
259
|
+
}
|
|
260
|
+
console.log(`Found ${filteredModules.length} modules to test:`);
|
|
261
|
+
for (const mod of filteredModules) {
|
|
262
|
+
console.log(` - ${mod.getModuleName()}`);
|
|
263
|
+
}
|
|
264
|
+
console.log('');
|
|
265
|
+
const failedPackages = [];
|
|
266
|
+
const successfulPackages = [];
|
|
267
|
+
for (const modulePkg of filteredModules) {
|
|
268
|
+
const result = await testModule(workspacePkg, modulePkg, fullCycle);
|
|
269
|
+
if (result.success) {
|
|
270
|
+
successfulPackages.push(result);
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
failedPackages.push(result);
|
|
274
|
+
if (stopOnFail) {
|
|
275
|
+
console.log('');
|
|
276
|
+
console.error(`${RED}STOPPING: Test failed for module ${result.moduleName} and --stop-on-fail was specified${NC}`);
|
|
277
|
+
console.log('');
|
|
278
|
+
console.log('=== TEST SUMMARY (PARTIAL) ===');
|
|
279
|
+
if (successfulPackages.length > 0) {
|
|
280
|
+
console.log(`${GREEN}Successful modules before failure (${successfulPackages.length}):${NC}`);
|
|
281
|
+
for (const pkg of successfulPackages) {
|
|
282
|
+
console.log(` ${GREEN}✓${NC} ${pkg.moduleName}`);
|
|
283
|
+
}
|
|
284
|
+
console.log('');
|
|
285
|
+
}
|
|
286
|
+
console.error(`${RED}Failed module: ${result.moduleName}${NC}`);
|
|
287
|
+
if (result.error) {
|
|
288
|
+
console.error(` Error: ${result.error}`);
|
|
289
|
+
}
|
|
290
|
+
console.log('');
|
|
291
|
+
console.error(`${RED}OVERALL RESULT: FAILED (stopped on first failure)${NC}`);
|
|
292
|
+
process.exit(1);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
console.log('');
|
|
296
|
+
}
|
|
297
|
+
console.log('=== TEST SUMMARY ===');
|
|
298
|
+
console.log(`${GREEN}Successful modules (${successfulPackages.length}):${NC}`);
|
|
299
|
+
for (const pkg of successfulPackages) {
|
|
300
|
+
console.log(` ${GREEN}✓${NC} ${pkg.moduleName}`);
|
|
301
|
+
}
|
|
302
|
+
if (failedPackages.length > 0) {
|
|
303
|
+
console.log('');
|
|
304
|
+
console.error(`${RED}Failed modules (${failedPackages.length}):${NC}`);
|
|
305
|
+
for (const pkg of failedPackages) {
|
|
306
|
+
console.error(` ${RED}✗${NC} ${pkg.moduleName}`);
|
|
307
|
+
if (pkg.error) {
|
|
308
|
+
console.error(` Error: ${pkg.error}`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
console.log('');
|
|
312
|
+
console.error(`${RED}OVERALL RESULT: FAILED${NC}`);
|
|
313
|
+
process.exit(1);
|
|
314
|
+
}
|
|
315
|
+
else {
|
|
316
|
+
console.log('');
|
|
317
|
+
console.log(`${GREEN}OVERALL RESULT: ALL MODULES PASSED${NC}`);
|
|
318
|
+
process.exit(0);
|
|
319
|
+
}
|
|
320
|
+
};
|
package/commands.js
CHANGED
|
@@ -27,6 +27,7 @@ const remove_1 = __importDefault(require("./commands/remove"));
|
|
|
27
27
|
const rename_1 = __importDefault(require("./commands/rename"));
|
|
28
28
|
const revert_1 = __importDefault(require("./commands/revert"));
|
|
29
29
|
const tag_1 = __importDefault(require("./commands/tag"));
|
|
30
|
+
const test_packages_1 = __importDefault(require("./commands/test-packages"));
|
|
30
31
|
const verify_1 = __importDefault(require("./commands/verify"));
|
|
31
32
|
const utils_1 = require("./utils");
|
|
32
33
|
const cli_error_1 = require("./utils/cli-error");
|
|
@@ -64,6 +65,7 @@ const createPgpmCommandMap = (skipPgTeardown = false) => {
|
|
|
64
65
|
migrate: pgt(migrate_1.default),
|
|
65
66
|
analyze: pgt(analyze_1.default),
|
|
66
67
|
rename: pgt(rename_1.default),
|
|
68
|
+
'test-packages': pgt(test_packages_1.default),
|
|
67
69
|
cache: cache_1.default,
|
|
68
70
|
update: update_1.default
|
|
69
71
|
};
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import { PgpmPackage } from '@pgpmjs/core';
|
|
2
|
+
import { getEnvOptions } from '@pgpmjs/env';
|
|
3
|
+
import { Logger } from '@pgpmjs/logger';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { getPgEnvOptions } from 'pg-env';
|
|
6
|
+
import { getPgPool } from 'pg-cache';
|
|
7
|
+
const log = new Logger('test-packages');
|
|
8
|
+
// ANSI color codes
|
|
9
|
+
const RED = '\x1b[0;31m';
|
|
10
|
+
const GREEN = '\x1b[0;32m';
|
|
11
|
+
const YELLOW = '\x1b[1;33m';
|
|
12
|
+
const NC = '\x1b[0m'; // No Color
|
|
13
|
+
const testPackagesUsageText = `
|
|
14
|
+
Test Packages Command:
|
|
15
|
+
|
|
16
|
+
pgpm test-packages [OPTIONS]
|
|
17
|
+
|
|
18
|
+
Run integration tests on all PGPM packages in a workspace.
|
|
19
|
+
Tests each package with a deploy/verify/revert/deploy cycle.
|
|
20
|
+
|
|
21
|
+
Options:
|
|
22
|
+
--help, -h Show this help message
|
|
23
|
+
--exclude <pkgs> Comma-separated module names to exclude
|
|
24
|
+
--stop-on-fail Stop testing immediately when a package fails
|
|
25
|
+
--full-cycle Run full deploy/verify/revert/deploy cycle (default: deploy only)
|
|
26
|
+
--cwd <directory> Working directory (default: current directory)
|
|
27
|
+
|
|
28
|
+
Examples:
|
|
29
|
+
pgpm test-packages Test all packages in workspace
|
|
30
|
+
pgpm test-packages --full-cycle Run full test cycle with verify/revert
|
|
31
|
+
pgpm test-packages --stop-on-fail Stop on first failure
|
|
32
|
+
pgpm test-packages --exclude my-module Exclude specific modules
|
|
33
|
+
`;
|
|
34
|
+
function dbSafeName(moduleName) {
|
|
35
|
+
return `test_${moduleName}`.replace(/[^a-zA-Z0-9]/g, '_').toLowerCase();
|
|
36
|
+
}
|
|
37
|
+
async function createDatabase(dbname, adminDb = 'postgres') {
|
|
38
|
+
try {
|
|
39
|
+
const pgEnv = getPgEnvOptions({ database: adminDb });
|
|
40
|
+
const pool = getPgPool(pgEnv);
|
|
41
|
+
// Sanitize database name (only allow alphanumeric and underscore)
|
|
42
|
+
const safeName = dbname.replace(/[^a-zA-Z0-9_]/g, '_');
|
|
43
|
+
await pool.query(`CREATE DATABASE "${safeName}"`);
|
|
44
|
+
log.debug(`Created database: ${safeName}`);
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
if (error.code === '42P04') {
|
|
49
|
+
// Database already exists, that's fine
|
|
50
|
+
log.debug(`Database ${dbname} already exists`);
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
log.error(`Failed to create database ${dbname}: ${error.message}`);
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
async function dropDatabase(dbname, adminDb = 'postgres') {
|
|
58
|
+
try {
|
|
59
|
+
const pgEnv = getPgEnvOptions({ database: adminDb });
|
|
60
|
+
const pool = getPgPool(pgEnv);
|
|
61
|
+
// Sanitize database name
|
|
62
|
+
const safeName = dbname.replace(/[^a-zA-Z0-9_]/g, '_');
|
|
63
|
+
// Terminate all connections to the database first
|
|
64
|
+
await pool.query(`
|
|
65
|
+
SELECT pg_terminate_backend(pid)
|
|
66
|
+
FROM pg_stat_activity
|
|
67
|
+
WHERE datname = $1 AND pid <> pg_backend_pid()
|
|
68
|
+
`, [safeName]);
|
|
69
|
+
await pool.query(`DROP DATABASE IF EXISTS "${safeName}"`);
|
|
70
|
+
log.debug(`Dropped database: ${safeName}`);
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
// Ignore errors when dropping (database might not exist)
|
|
74
|
+
log.debug(`Could not drop database ${dbname}: ${error.message}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async function checkPostgresConnection() {
|
|
78
|
+
try {
|
|
79
|
+
const pgEnv = getPgEnvOptions({ database: 'postgres' });
|
|
80
|
+
const pool = getPgPool(pgEnv);
|
|
81
|
+
await pool.query('SELECT 1');
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
async function testModule(workspacePkg, modulePkg, fullCycle) {
|
|
89
|
+
const moduleName = modulePkg.getModuleName();
|
|
90
|
+
const modulePath = modulePkg.getModulePath() || '';
|
|
91
|
+
const dbname = dbSafeName(moduleName);
|
|
92
|
+
console.log(`${YELLOW}Testing module: ${moduleName}${NC}`);
|
|
93
|
+
console.log(` Module path: ${modulePath}`);
|
|
94
|
+
console.log(` Database name: ${dbname}`);
|
|
95
|
+
// Clean up any existing test database
|
|
96
|
+
await dropDatabase(dbname);
|
|
97
|
+
// Create fresh test database
|
|
98
|
+
console.log(` Creating database: ${dbname}`);
|
|
99
|
+
if (!await createDatabase(dbname)) {
|
|
100
|
+
return {
|
|
101
|
+
moduleName,
|
|
102
|
+
modulePath,
|
|
103
|
+
success: false,
|
|
104
|
+
error: `Could not create database ${dbname}`
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
// Create options for this test database
|
|
109
|
+
const opts = getEnvOptions({
|
|
110
|
+
pg: getPgEnvOptions({ database: dbname }),
|
|
111
|
+
deployment: {
|
|
112
|
+
useTx: true,
|
|
113
|
+
fast: false,
|
|
114
|
+
usePlan: true,
|
|
115
|
+
cache: false,
|
|
116
|
+
logOnly: false
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
// Deploy
|
|
120
|
+
console.log(' Running deploy...');
|
|
121
|
+
try {
|
|
122
|
+
await workspacePkg.deploy(opts, moduleName, true);
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
await dropDatabase(dbname);
|
|
126
|
+
return {
|
|
127
|
+
moduleName,
|
|
128
|
+
modulePath,
|
|
129
|
+
success: false,
|
|
130
|
+
error: `Deploy failed: ${error.message}`
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
if (fullCycle) {
|
|
134
|
+
// Verify
|
|
135
|
+
console.log(' Running verify...');
|
|
136
|
+
try {
|
|
137
|
+
await workspacePkg.verify(opts, moduleName, true);
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
await dropDatabase(dbname);
|
|
141
|
+
return {
|
|
142
|
+
moduleName,
|
|
143
|
+
modulePath,
|
|
144
|
+
success: false,
|
|
145
|
+
error: `Verify failed: ${error.message}`
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
// Revert
|
|
149
|
+
console.log(' Running revert...');
|
|
150
|
+
try {
|
|
151
|
+
await workspacePkg.revert(opts, moduleName, true);
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
await dropDatabase(dbname);
|
|
155
|
+
return {
|
|
156
|
+
moduleName,
|
|
157
|
+
modulePath,
|
|
158
|
+
success: false,
|
|
159
|
+
error: `Revert failed: ${error.message}`
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
// Deploy again
|
|
163
|
+
console.log(' Running deploy (second time)...');
|
|
164
|
+
try {
|
|
165
|
+
await workspacePkg.deploy(opts, moduleName, true);
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
await dropDatabase(dbname);
|
|
169
|
+
return {
|
|
170
|
+
moduleName,
|
|
171
|
+
modulePath,
|
|
172
|
+
success: false,
|
|
173
|
+
error: `Deploy (second time) failed: ${error.message}`
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
// Clean up test database
|
|
178
|
+
await dropDatabase(dbname);
|
|
179
|
+
console.log(`${GREEN}SUCCESS: Module ${moduleName} passed all tests${NC}`);
|
|
180
|
+
return {
|
|
181
|
+
moduleName,
|
|
182
|
+
modulePath,
|
|
183
|
+
success: true
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
catch (error) {
|
|
187
|
+
// Ensure cleanup on any unexpected error
|
|
188
|
+
await dropDatabase(dbname);
|
|
189
|
+
return {
|
|
190
|
+
moduleName,
|
|
191
|
+
modulePath,
|
|
192
|
+
success: false,
|
|
193
|
+
error: `Unexpected error: ${error.message}`
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
export default async (argv, _prompter, _options) => {
|
|
198
|
+
// Show usage if explicitly requested
|
|
199
|
+
if (argv.help || argv.h) {
|
|
200
|
+
console.log(testPackagesUsageText);
|
|
201
|
+
process.exit(0);
|
|
202
|
+
}
|
|
203
|
+
// Parse options
|
|
204
|
+
const stopOnFail = argv['stop-on-fail'] === true || argv.stopOnFail === true;
|
|
205
|
+
const fullCycle = argv['full-cycle'] === true || argv.fullCycle === true;
|
|
206
|
+
const cwd = argv.cwd || process.cwd();
|
|
207
|
+
// Parse excludes
|
|
208
|
+
let excludes = [];
|
|
209
|
+
if (argv.exclude) {
|
|
210
|
+
excludes = argv.exclude.split(',').map(e => e.trim());
|
|
211
|
+
}
|
|
212
|
+
console.log('=== PGPM Package Integration Test ===');
|
|
213
|
+
console.log(`Testing all packages with ${fullCycle ? 'deploy/verify/revert/deploy cycle' : 'deploy only'}`);
|
|
214
|
+
if (stopOnFail) {
|
|
215
|
+
console.log('Mode: Stop on first failure');
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
console.log('Mode: Test all packages (collect all failures)');
|
|
219
|
+
}
|
|
220
|
+
console.log('');
|
|
221
|
+
// Check PostgreSQL connection
|
|
222
|
+
console.log('Checking PostgreSQL connection...');
|
|
223
|
+
if (!await checkPostgresConnection()) {
|
|
224
|
+
log.error('PostgreSQL not accessible.');
|
|
225
|
+
console.log('Ensure PostgreSQL is running and connection settings are correct.');
|
|
226
|
+
console.log('For local development: docker-compose up -d');
|
|
227
|
+
console.log('For CI: ensure PostgreSQL service is running');
|
|
228
|
+
process.exit(1);
|
|
229
|
+
}
|
|
230
|
+
console.log('PostgreSQL connection successful');
|
|
231
|
+
console.log('');
|
|
232
|
+
// Initialize workspace package
|
|
233
|
+
const projectRoot = path.resolve(cwd);
|
|
234
|
+
const workspacePkg = new PgpmPackage(projectRoot);
|
|
235
|
+
if (!workspacePkg.getWorkspacePath()) {
|
|
236
|
+
log.error('Not in a PGPM workspace. Run this command from a workspace root.');
|
|
237
|
+
process.exit(1);
|
|
238
|
+
}
|
|
239
|
+
// Get all modules from workspace using internal API
|
|
240
|
+
console.log('Finding all PGPM modules in workspace...');
|
|
241
|
+
const modules = await workspacePkg.getModules();
|
|
242
|
+
if (modules.length === 0) {
|
|
243
|
+
log.warn('No modules found in workspace.');
|
|
244
|
+
process.exit(0);
|
|
245
|
+
}
|
|
246
|
+
// Filter out excluded modules
|
|
247
|
+
let filteredModules = modules;
|
|
248
|
+
if (excludes.length > 0) {
|
|
249
|
+
filteredModules = modules.filter(mod => {
|
|
250
|
+
const moduleName = mod.getModuleName();
|
|
251
|
+
return !excludes.includes(moduleName);
|
|
252
|
+
});
|
|
253
|
+
console.log(`Excluding: ${excludes.join(', ')}`);
|
|
254
|
+
}
|
|
255
|
+
console.log(`Found ${filteredModules.length} modules to test:`);
|
|
256
|
+
for (const mod of filteredModules) {
|
|
257
|
+
console.log(` - ${mod.getModuleName()}`);
|
|
258
|
+
}
|
|
259
|
+
console.log('');
|
|
260
|
+
const failedPackages = [];
|
|
261
|
+
const successfulPackages = [];
|
|
262
|
+
for (const modulePkg of filteredModules) {
|
|
263
|
+
const result = await testModule(workspacePkg, modulePkg, fullCycle);
|
|
264
|
+
if (result.success) {
|
|
265
|
+
successfulPackages.push(result);
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
failedPackages.push(result);
|
|
269
|
+
if (stopOnFail) {
|
|
270
|
+
console.log('');
|
|
271
|
+
console.error(`${RED}STOPPING: Test failed for module ${result.moduleName} and --stop-on-fail was specified${NC}`);
|
|
272
|
+
console.log('');
|
|
273
|
+
console.log('=== TEST SUMMARY (PARTIAL) ===');
|
|
274
|
+
if (successfulPackages.length > 0) {
|
|
275
|
+
console.log(`${GREEN}Successful modules before failure (${successfulPackages.length}):${NC}`);
|
|
276
|
+
for (const pkg of successfulPackages) {
|
|
277
|
+
console.log(` ${GREEN}✓${NC} ${pkg.moduleName}`);
|
|
278
|
+
}
|
|
279
|
+
console.log('');
|
|
280
|
+
}
|
|
281
|
+
console.error(`${RED}Failed module: ${result.moduleName}${NC}`);
|
|
282
|
+
if (result.error) {
|
|
283
|
+
console.error(` Error: ${result.error}`);
|
|
284
|
+
}
|
|
285
|
+
console.log('');
|
|
286
|
+
console.error(`${RED}OVERALL RESULT: FAILED (stopped on first failure)${NC}`);
|
|
287
|
+
process.exit(1);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
console.log('');
|
|
291
|
+
}
|
|
292
|
+
console.log('=== TEST SUMMARY ===');
|
|
293
|
+
console.log(`${GREEN}Successful modules (${successfulPackages.length}):${NC}`);
|
|
294
|
+
for (const pkg of successfulPackages) {
|
|
295
|
+
console.log(` ${GREEN}✓${NC} ${pkg.moduleName}`);
|
|
296
|
+
}
|
|
297
|
+
if (failedPackages.length > 0) {
|
|
298
|
+
console.log('');
|
|
299
|
+
console.error(`${RED}Failed modules (${failedPackages.length}):${NC}`);
|
|
300
|
+
for (const pkg of failedPackages) {
|
|
301
|
+
console.error(` ${RED}✗${NC} ${pkg.moduleName}`);
|
|
302
|
+
if (pkg.error) {
|
|
303
|
+
console.error(` Error: ${pkg.error}`);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
console.log('');
|
|
307
|
+
console.error(`${RED}OVERALL RESULT: FAILED${NC}`);
|
|
308
|
+
process.exit(1);
|
|
309
|
+
}
|
|
310
|
+
else {
|
|
311
|
+
console.log('');
|
|
312
|
+
console.log(`${GREEN}OVERALL RESULT: ALL MODULES PASSED${NC}`);
|
|
313
|
+
process.exit(0);
|
|
314
|
+
}
|
|
315
|
+
};
|
package/esm/commands.js
CHANGED
|
@@ -21,6 +21,7 @@ import remove from './commands/remove';
|
|
|
21
21
|
import renameCmd from './commands/rename';
|
|
22
22
|
import revert from './commands/revert';
|
|
23
23
|
import tag from './commands/tag';
|
|
24
|
+
import testPackages from './commands/test-packages';
|
|
24
25
|
import verify from './commands/verify';
|
|
25
26
|
import { extractFirst, usageText } from './utils';
|
|
26
27
|
import { cliExitWithError } from './utils/cli-error';
|
|
@@ -58,6 +59,7 @@ export const createPgpmCommandMap = (skipPgTeardown = false) => {
|
|
|
58
59
|
migrate: pgt(migrate),
|
|
59
60
|
analyze: pgt(analyze),
|
|
60
61
|
rename: pgt(renameCmd),
|
|
62
|
+
'test-packages': pgt(testPackages),
|
|
61
63
|
cache,
|
|
62
64
|
update: updateCmd
|
|
63
65
|
};
|
package/esm/index.js
CHANGED
|
@@ -23,6 +23,7 @@ export { default as remove } from './commands/remove';
|
|
|
23
23
|
export { default as renameCmd } from './commands/rename';
|
|
24
24
|
export { default as revert } from './commands/revert';
|
|
25
25
|
export { default as tag } from './commands/tag';
|
|
26
|
+
export { default as testPackages } from './commands/test-packages';
|
|
26
27
|
export { default as verify } from './commands/verify';
|
|
27
28
|
export * from './utils';
|
|
28
29
|
export const options = {
|
package/index.d.ts
CHANGED
|
@@ -21,6 +21,7 @@ export { default as remove } from './commands/remove';
|
|
|
21
21
|
export { default as renameCmd } from './commands/rename';
|
|
22
22
|
export { default as revert } from './commands/revert';
|
|
23
23
|
export { default as tag } from './commands/tag';
|
|
24
|
+
export { default as testPackages } from './commands/test-packages';
|
|
24
25
|
export { default as verify } from './commands/verify';
|
|
25
26
|
export * from './utils';
|
|
26
27
|
export declare const options: Partial<CLIOptions>;
|
package/index.js
CHANGED
|
@@ -18,7 +18,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
18
18
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
19
19
|
};
|
|
20
20
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
-
exports.options = exports.verify = exports.tag = exports.revert = exports.renameCmd = exports.remove = exports.plan = exports._package = exports.migrate = exports.kill = exports.install = exports.extension = exports._export = exports.env = exports.docker = exports.deploy = exports.clear = exports.analyze = exports.adminUsers = exports.add = exports.createPgpmCommandMap = exports.createInitUsageText = void 0;
|
|
21
|
+
exports.options = exports.verify = exports.testPackages = exports.tag = exports.revert = exports.renameCmd = exports.remove = exports.plan = exports._package = exports.migrate = exports.kill = exports.install = exports.extension = exports._export = exports.env = exports.docker = exports.deploy = exports.clear = exports.analyze = exports.adminUsers = exports.add = exports.createPgpmCommandMap = exports.createInitUsageText = void 0;
|
|
22
22
|
const fs_1 = require("fs");
|
|
23
23
|
const inquirerer_1 = require("inquirerer");
|
|
24
24
|
const path_1 = require("path");
|
|
@@ -62,6 +62,8 @@ var revert_1 = require("./commands/revert");
|
|
|
62
62
|
Object.defineProperty(exports, "revert", { enumerable: true, get: function () { return __importDefault(revert_1).default; } });
|
|
63
63
|
var tag_1 = require("./commands/tag");
|
|
64
64
|
Object.defineProperty(exports, "tag", { enumerable: true, get: function () { return __importDefault(tag_1).default; } });
|
|
65
|
+
var test_packages_1 = require("./commands/test-packages");
|
|
66
|
+
Object.defineProperty(exports, "testPackages", { enumerable: true, get: function () { return __importDefault(test_packages_1).default; } });
|
|
65
67
|
var verify_1 = require("./commands/verify");
|
|
66
68
|
Object.defineProperty(exports, "verify", { enumerable: true, get: function () { return __importDefault(verify_1).default; } });
|
|
67
69
|
__exportStar(require("./utils"), exports);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pgpm",
|
|
3
|
-
"version": "1.1
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"author": "Constructive <developers@constructive.io>",
|
|
5
5
|
"description": "PostgreSQL Package Manager - Database migration and package management CLI",
|
|
6
6
|
"main": "index.js",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"ts-node": "^10.9.2"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@pgpmjs/core": "^3.1.
|
|
48
|
+
"@pgpmjs/core": "^3.1.4",
|
|
49
49
|
"@pgpmjs/env": "^2.8.8",
|
|
50
50
|
"@pgpmjs/logger": "^1.3.5",
|
|
51
51
|
"@pgpmjs/types": "^2.12.6",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"inquirerer": "^2.2.0",
|
|
56
56
|
"js-yaml": "^4.1.0",
|
|
57
57
|
"minimist": "^1.2.8",
|
|
58
|
-
"pg-cache": "^1.6.
|
|
58
|
+
"pg-cache": "^1.6.9",
|
|
59
59
|
"pg-env": "^1.2.4",
|
|
60
60
|
"semver": "^7.6.2",
|
|
61
61
|
"shelljs": "^0.10.0",
|
|
@@ -73,5 +73,5 @@
|
|
|
73
73
|
"pg",
|
|
74
74
|
"pgsql"
|
|
75
75
|
],
|
|
76
|
-
"gitHead": "
|
|
76
|
+
"gitHead": "fc182ff9e4c4745a3e86eda6d58e3b0061f36564"
|
|
77
77
|
}
|