Skip to content

Commit f73c89f

Browse files
authored
Merge pull request #1559 from constructive-io/feat/pgpm-package-check
feat(pgpm): `pgpm package --check` — verify committed bundle artifacts are in sync with deploy/
2 parents 6bb197a + c0b3b0a commit f73c89f

4 files changed

Lines changed: 473 additions & 1 deletion

File tree

pgpm/cli/src/commands/package.ts

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1-
import { PgpmPackage, writePackage } from '@pgpmjs/core';
1+
import { checkPackages, PgpmPackage, writePackage } from '@pgpmjs/core';
2+
import { Logger } from '@pgpmjs/logger';
23
import { CLIOptions, Inquirerer, Question } from 'inquirerer';
34

5+
const log = new Logger('package');
6+
47
const packageUsageText = `
58
Package Command:
69
@@ -17,11 +20,23 @@ Options:
1720
--bundle Also emit sql/<name>--<version>.bundle.tar.gz (default: true)
1821
--cwd <directory> Working directory (default: current directory)
1922
23+
Check mode (no writes — verify committed artifacts are in sync with deploy/):
24+
--check Verify committed sql/<name>--<version>.bundle.tar.gz
25+
matches deploy/ for changed modules; exit 1 on drift
26+
--since <ref> Git branch/ref/tag to diff HEAD against for change
27+
detection (default: PR base in CI, else working tree)
28+
--all Check every workspace module (skip change detection)
29+
--dependents Also re-check modules that require a changed module
30+
--no-fail-fast Report all drifted modules instead of stopping at the first
31+
2032
Examples:
2133
pgpm package Package with defaults
2234
pgpm package --no-plan Package without plan
2335
pgpm package --outputDiff Package and export AST diff files if mismatch detected
2436
pgpm package --no-bundle Package without emitting the bundle artifact
37+
pgpm package --check Verify changed modules' bundles are in sync
38+
pgpm package --check --since origin/main Verify modules changed vs origin/main
39+
pgpm package --check --all Verify every module in the workspace
2540
`;
2641

2742
export default async (
@@ -34,6 +49,46 @@ export default async (
3449
console.log(packageUsageText);
3550
process.exit(0);
3651
}
52+
53+
// Check mode: verify committed artifacts, never write.
54+
if (argv.check) {
55+
const result = await checkPackages({
56+
cwd: argv.cwd,
57+
since: typeof argv.since === 'string' ? argv.since : undefined,
58+
all: argv.all === true,
59+
dependents: argv.dependents === true,
60+
failFast: argv.failFast !== false,
61+
});
62+
63+
if (argv.all) {
64+
log.info(`Checking all ${result.targeted.length} workspace module(s)`);
65+
} else {
66+
log.info(
67+
`Change detection${result.base ? ` vs ${result.base}` : ' (working tree)'}: ` +
68+
`${result.changedModules.length} changed module(s)` +
69+
(result.targeted.length !== result.changedModules.length
70+
? ` (${result.targeted.length} incl. dependents)`
71+
: '')
72+
);
73+
}
74+
75+
if (result.drifted.length) {
76+
for (const drift of result.drifted) {
77+
log.error(`✖ ${drift.name}: ${drift.detail}`);
78+
}
79+
log.error(
80+
`${result.drifted.length} module(s) out of sync. Run \`pgpm package\` in each and commit the artifacts.`
81+
);
82+
process.exit(1);
83+
}
84+
85+
if (result.checked.length === 0) {
86+
log.success('No changed modules to check.');
87+
} else {
88+
log.success(`${result.checked.length} module(s) in sync.`);
89+
}
90+
return argv;
91+
}
3792
const questions: Question[] = [
3893
{
3994
type: 'confirm',
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import { readFileSync, rmSync, writeFileSync } from 'fs';
2+
import { join } from 'path';
3+
4+
import {
5+
addDependents,
6+
checkModuleArtifact,
7+
checkPackages,
8+
enumerateModules,
9+
mapFilesToModules,
10+
} from '../../src/packaging/check';
11+
import { resolveBundleArtifactPath, writeBundleArtifact } from '../../src/bundle/artifact';
12+
import { PgpmPackage } from '../../src/core/class/pgpm';
13+
import { TestFixture } from '../../test-utils';
14+
15+
const MODULES = ['my-first', 'my-second', 'my-third'] as const;
16+
17+
/**
18+
* `pgpm package --check`: verify a committed bundle artifact still matches its
19+
* `deploy/`. Read + sha256 only — no DDL, no database — so these are pure,
20+
* DB-free unit tests over the fixture workspace.
21+
*/
22+
describe('package check (artifact drift verification)', () => {
23+
let fixture: TestFixture;
24+
let root: string;
25+
26+
const moduleDir = (name: string): string => fixture.fixturePath('packages', name);
27+
28+
const emitAll = async (): Promise<void> => {
29+
for (const name of MODULES) {
30+
const dir = moduleDir(name);
31+
const version = require(join(dir, 'package.json')).version as string;
32+
await writeBundleArtifact(dir, version);
33+
}
34+
};
35+
36+
beforeEach(() => {
37+
fixture = new TestFixture('sqitch', 'simple-w-tags');
38+
root = fixture.fixturePath();
39+
});
40+
41+
afterEach(() => {
42+
fixture.cleanup();
43+
});
44+
45+
it('reports no drift for a freshly packaged module', async () => {
46+
await emitAll();
47+
expect(checkModuleArtifact(moduleDir('my-first'), 'my-first')).toBeNull();
48+
});
49+
50+
it('flags a missing artifact', () => {
51+
const drift = checkModuleArtifact(moduleDir('my-first'), 'my-first');
52+
expect(drift?.reason).toBe('missing-artifact');
53+
});
54+
55+
it('flags an artifact whose deploy/ has changed (out of sync)', async () => {
56+
await emitAll();
57+
const dir = moduleDir('my-first');
58+
const deployFile = join(dir, 'deploy', 'schema_myfirstapp.sql');
59+
writeFileSync(deployFile, `${readFileSync(deployFile, 'utf-8')}\n-- drift\n`);
60+
61+
const drift = checkModuleArtifact(dir, 'my-first');
62+
expect(drift?.reason).toBe('out-of-sync');
63+
});
64+
65+
it('flags a corrupt artifact archive', async () => {
66+
await emitAll();
67+
writeFileSync(resolveBundleArtifactPath(moduleDir('my-second'))!, 'not-an-archive');
68+
const drift = checkModuleArtifact(moduleDir('my-second'), 'my-second');
69+
expect(drift?.reason).toBe('unreadable-artifact');
70+
});
71+
72+
it('checkPackages --all verifies every module and passes when in sync', async () => {
73+
await emitAll();
74+
const result = await checkPackages({ cwd: root, all: true });
75+
expect(new Set(result.checked)).toEqual(new Set(MODULES));
76+
expect(result.drifted).toHaveLength(0);
77+
});
78+
79+
it('checkPackages --all fails fast on the first drift by default', async () => {
80+
await emitAll();
81+
// Break two modules; fail-fast should report exactly one.
82+
rmSync(resolveBundleArtifactPath(moduleDir('my-first'))!);
83+
rmSync(resolveBundleArtifactPath(moduleDir('my-second'))!);
84+
const result = await checkPackages({ cwd: root, all: true });
85+
expect(result.drifted).toHaveLength(1);
86+
});
87+
88+
it('checkPackages --all --no-fail-fast reports every drifted module', async () => {
89+
await emitAll();
90+
rmSync(resolveBundleArtifactPath(moduleDir('my-first'))!);
91+
rmSync(resolveBundleArtifactPath(moduleDir('my-second'))!);
92+
const result = await checkPackages({ cwd: root, all: true, failFast: false });
93+
expect(new Set(result.drifted.map((d) => d.name))).toEqual(
94+
new Set(['my-first', 'my-second'])
95+
);
96+
});
97+
});
98+
99+
describe('package check (workspace mapping helpers)', () => {
100+
let fixture: TestFixture;
101+
let pkg: PgpmPackage;
102+
103+
beforeEach(() => {
104+
fixture = new TestFixture('sqitch', 'simple-w-tags');
105+
pkg = new PgpmPackage(fixture.fixturePath());
106+
});
107+
108+
afterEach(() => {
109+
fixture.cleanup();
110+
});
111+
112+
it('maps a changed deploy file to its owning module', () => {
113+
const modules = enumerateModules(pkg);
114+
const changed = [fixture.fixturePath('packages', 'my-second', 'deploy', 'anything.sql')];
115+
expect(mapFilesToModules(changed, modules)).toEqual(['my-second']);
116+
});
117+
118+
it('ignores files outside any module', () => {
119+
const modules = enumerateModules(pkg);
120+
expect(mapFilesToModules([fixture.fixturePath('README.md')], modules)).toEqual([]);
121+
});
122+
123+
it('expands to transitive dependents', () => {
124+
// my-third requires my-second requires my-first.
125+
const names = new Set(['my-first']);
126+
addDependents(names, pkg);
127+
expect(names).toEqual(new Set(['my-first', 'my-second', 'my-third']));
128+
});
129+
});

pgpm/core/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export * from './core/class/pgpm';
22
export * from './rebundle';
33
export * from './extensions';
44
export * from './modules/modules';
5+
export * from './packaging/check';
56
export * from './packaging/package';
67
export * from './packaging/sync-versions';
78
export * from './packaging/transform';

0 commit comments

Comments
 (0)