@lightdash/cli 1.125.0 → 1.126.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/dist/.tsbuildinfo +1 -1
- package/dist/handlers/upgradeCheck.d.ts +30 -0
- package/dist/handlers/upgradeCheck.d.ts.map +1 -0
- package/dist/handlers/upgradeCheck.js +120 -0
- package/dist/handlers/upgradeCheck.test.d.ts +2 -0
- package/dist/handlers/upgradeCheck.test.d.ts.map +1 -0
- package/dist/handlers/upgradeCheck.test.js +227 -0
- package/dist/index.js +2 -0
- package/dist/releaseSafety/index.d.ts +34 -0
- package/dist/releaseSafety/index.d.ts.map +1 -0
- package/dist/releaseSafety/index.js +111 -0
- package/dist/releaseSafety/index.test.d.ts +2 -0
- package/dist/releaseSafety/index.test.d.ts.map +1 -0
- package/dist/releaseSafety/index.test.js +154 -0
- package/dist/releaseSafety/release-safety-index.schema.json +69 -0
- package/dist/releaseSafety/version.d.ts +3 -0
- package/dist/releaseSafety/version.d.ts.map +1 -0
- package/dist/releaseSafety/version.js +28 -0
- package/dist/releaseSafety/version.test.d.ts +2 -0
- package/dist/releaseSafety/version.test.d.ts.map +1 -0
- package/dist/releaseSafety/version.test.js +22 -0
- package/package.json +3 -3
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Command } from 'commander';
|
|
2
|
+
import fetch from 'node-fetch';
|
|
3
|
+
import { type ReleaseSafetyIndex, type ReleaseSafetyMissingRange, type TriState } from '../releaseSafety';
|
|
4
|
+
export declare const RELEASE_SAFETY_INDEX_URL = "https://raw.githubusercontent.com/lightdash/lightdash/main/release-safety-index.json";
|
|
5
|
+
export interface UpgradeCheckOptions {
|
|
6
|
+
from: string;
|
|
7
|
+
to: string;
|
|
8
|
+
json?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export type UpgradeCheckDirection = 'upgrade' | 'same-version';
|
|
11
|
+
export interface UpgradeCheckOutput {
|
|
12
|
+
fromVersion: string;
|
|
13
|
+
toVersion: string;
|
|
14
|
+
direction: UpgradeCheckDirection;
|
|
15
|
+
safe: boolean;
|
|
16
|
+
verdict: TriState;
|
|
17
|
+
requiredStops: string[];
|
|
18
|
+
minPreviousVersion: string | null;
|
|
19
|
+
coveredVersions: string[];
|
|
20
|
+
missingRanges: ReleaseSafetyMissingRange[];
|
|
21
|
+
}
|
|
22
|
+
export interface UpgradeCheckDependencies {
|
|
23
|
+
fetch: typeof fetch;
|
|
24
|
+
writeOutput: (output: string) => void;
|
|
25
|
+
exit: (code: number) => never;
|
|
26
|
+
}
|
|
27
|
+
export declare function renderUpgradeCheckOutput(output: UpgradeCheckOutput, index: ReleaseSafetyIndex): string;
|
|
28
|
+
export declare function upgradeCheckHandler(options: UpgradeCheckOptions, dependencies?: Partial<UpgradeCheckDependencies>): Promise<UpgradeCheckOutput>;
|
|
29
|
+
export declare function registerUpgradeCheckCommand(command: Command, dependencies?: Partial<UpgradeCheckDependencies>): Command;
|
|
30
|
+
//# sourceMappingURL=upgradeCheck.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"upgradeCheck.d.ts","sourceRoot":"","sources":["../../src/handlers/upgradeCheck.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,MAAM,YAAY,CAAC;AAC/B,OAAO,EAKH,KAAK,kBAAkB,EACvB,KAAK,yBAAyB,EAC9B,KAAK,QAAQ,EAChB,MAAM,kBAAkB,CAAC;AAE1B,eAAO,MAAM,wBAAwB,yFACqD,CAAC;AAE3F,MAAM,WAAW,mBAAmB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,MAAM,qBAAqB,GAAG,SAAS,GAAG,cAAc,CAAC;AAE/D,MAAM,WAAW,kBAAkB;IAC/B,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,qBAAqB,CAAC;IACjC,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,QAAQ,CAAC;IAClB,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,aAAa,EAAE,yBAAyB,EAAE,CAAC;CAC9C;AAED,MAAM,WAAW,wBAAwB;IACrC,KAAK,EAAE,OAAO,KAAK,CAAC;IACpB,WAAW,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACtC,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,KAAK,CAAC;CACjC;AA+DD,wBAAgB,wBAAwB,CACpC,MAAM,EAAE,kBAAkB,EAC1B,KAAK,EAAE,kBAAkB,GAC1B,MAAM,CAkCR;AAED,wBAAsB,mBAAmB,CACrC,OAAO,EAAE,mBAAmB,EAC5B,YAAY,GAAE,OAAO,CAAC,wBAAwB,CAAM,GACrD,OAAO,CAAC,kBAAkB,CAAC,CAwC7B;AAED,wBAAgB,2BAA2B,CACvC,OAAO,EAAE,OAAO,EAChB,YAAY,GAAE,OAAO,CAAC,wBAAwB,CAAM,GACrD,OAAO,CAYT"}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RELEASE_SAFETY_INDEX_URL = void 0;
|
|
4
|
+
exports.renderUpgradeCheckOutput = renderUpgradeCheckOutput;
|
|
5
|
+
exports.upgradeCheckHandler = upgradeCheckHandler;
|
|
6
|
+
exports.registerUpgradeCheckCommand = registerUpgradeCheckCommand;
|
|
7
|
+
const tslib_1 = require("tslib");
|
|
8
|
+
const node_fetch_1 = tslib_1.__importDefault(require("node-fetch"));
|
|
9
|
+
const releaseSafety_1 = require("../releaseSafety");
|
|
10
|
+
exports.RELEASE_SAFETY_INDEX_URL = 'https://raw.githubusercontent.com/lightdash/lightdash/main/release-safety-index.json';
|
|
11
|
+
const defaultDependencies = {
|
|
12
|
+
fetch: node_fetch_1.default,
|
|
13
|
+
writeOutput: (output) => console.log(output),
|
|
14
|
+
exit: (code) => process.exit(code),
|
|
15
|
+
};
|
|
16
|
+
function getDirection(fromVersion, toVersion) {
|
|
17
|
+
const comparison = (0, releaseSafety_1.compareVersions)(fromVersion, toVersion);
|
|
18
|
+
if (comparison < 0) {
|
|
19
|
+
return 'upgrade';
|
|
20
|
+
}
|
|
21
|
+
if (comparison > 0) {
|
|
22
|
+
throw new Error('Rollback spans are not supported by this command; rollback guidance ships in the upgrade runbook.');
|
|
23
|
+
}
|
|
24
|
+
return 'same-version';
|
|
25
|
+
}
|
|
26
|
+
function validateEndpoint(name, version) {
|
|
27
|
+
if (!(0, releaseSafety_1.isReleaseVersion)(version)) {
|
|
28
|
+
throw new Error(`--${name} must be a release version in X.Y.Z format, received ${version}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function formatMissingRange(range) {
|
|
32
|
+
if (range.afterVersion === range.beforeVersion) {
|
|
33
|
+
return `version ${range.afterVersion} is not present in the release-safety index`;
|
|
34
|
+
}
|
|
35
|
+
return `after ${range.afterVersion} and before ${range.beforeVersion}`;
|
|
36
|
+
}
|
|
37
|
+
function findReleaseStates(index, output) {
|
|
38
|
+
const coveredVersions = new Set(output.coveredVersions);
|
|
39
|
+
return index.entries.reduce((states, entry) => {
|
|
40
|
+
if (!coveredVersions.has(entry.version)) {
|
|
41
|
+
return states;
|
|
42
|
+
}
|
|
43
|
+
if (entry.rollingUpdateSafe === false) {
|
|
44
|
+
states.falseVersions.push(entry.version);
|
|
45
|
+
}
|
|
46
|
+
else if (entry.rollingUpdateSafe === 'unknown') {
|
|
47
|
+
states.unknownVersions.push(entry.version);
|
|
48
|
+
}
|
|
49
|
+
return states;
|
|
50
|
+
}, { falseVersions: [], unknownVersions: [] });
|
|
51
|
+
}
|
|
52
|
+
function renderUpgradeCheckOutput(output, index) {
|
|
53
|
+
const lines = [
|
|
54
|
+
`Release safety check: ${output.fromVersion} -> ${output.toVersion}`,
|
|
55
|
+
`Direction: ${output.direction}`,
|
|
56
|
+
`Verdict: ${output.safe ? 'SAFE' : 'UNSAFE'}`,
|
|
57
|
+
];
|
|
58
|
+
const { falseVersions, unknownVersions } = findReleaseStates(index, output);
|
|
59
|
+
if (falseVersions.length > 0) {
|
|
60
|
+
lines.push(`Unsafe rolling-update safety: ${falseVersions.join(', ')}`);
|
|
61
|
+
}
|
|
62
|
+
if (unknownVersions.length > 0) {
|
|
63
|
+
lines.push(`Unknown rolling-update safety: ${unknownVersions.join(', ')}`);
|
|
64
|
+
}
|
|
65
|
+
if (output.requiredStops.length > 0) {
|
|
66
|
+
lines.push(`Required stops: ${output.requiredStops.join(', ')}`);
|
|
67
|
+
}
|
|
68
|
+
if (output.minPreviousVersion !== null) {
|
|
69
|
+
lines.push(`Minimum previous version: ${output.minPreviousVersion}`);
|
|
70
|
+
}
|
|
71
|
+
if (output.missingRanges.length > 0) {
|
|
72
|
+
lines.push('Missing release-safety index coverage:');
|
|
73
|
+
lines.push(...output.missingRanges.map((range) => ` - ${formatMissingRange(range)}`));
|
|
74
|
+
}
|
|
75
|
+
if (output.coveredVersions.length > 0) {
|
|
76
|
+
lines.push(`Covered releases: ${output.coveredVersions.join(', ')}`);
|
|
77
|
+
}
|
|
78
|
+
return lines.join('\n');
|
|
79
|
+
}
|
|
80
|
+
async function upgradeCheckHandler(options, dependencies = {}) {
|
|
81
|
+
const resolvedDependencies = { ...defaultDependencies, ...dependencies };
|
|
82
|
+
validateEndpoint('from', options.from);
|
|
83
|
+
validateEndpoint('to', options.to);
|
|
84
|
+
const direction = getDirection(options.from, options.to);
|
|
85
|
+
const response = await resolvedDependencies.fetch(exports.RELEASE_SAFETY_INDEX_URL);
|
|
86
|
+
if (!response.ok) {
|
|
87
|
+
throw new Error(`Unable to fetch the release-safety index: ${response.status} ${response.statusText}`);
|
|
88
|
+
}
|
|
89
|
+
const index = (0, releaseSafety_1.parseReleaseSafetyIndex)(await response.json());
|
|
90
|
+
const composition = (0, releaseSafety_1.composeReleaseSafetySpan)(index, options.from, options.to);
|
|
91
|
+
const output = {
|
|
92
|
+
fromVersion: options.from,
|
|
93
|
+
toVersion: options.to,
|
|
94
|
+
direction,
|
|
95
|
+
safe: composition.safe,
|
|
96
|
+
verdict: composition.verdict,
|
|
97
|
+
requiredStops: composition.requiredStops,
|
|
98
|
+
minPreviousVersion: composition.minPreviousVersion,
|
|
99
|
+
coveredVersions: composition.coveredVersions,
|
|
100
|
+
missingRanges: composition.missingRanges,
|
|
101
|
+
};
|
|
102
|
+
resolvedDependencies.writeOutput(options.json
|
|
103
|
+
? JSON.stringify(output)
|
|
104
|
+
: renderUpgradeCheckOutput(output, index));
|
|
105
|
+
if (!output.safe) {
|
|
106
|
+
resolvedDependencies.exit(1);
|
|
107
|
+
}
|
|
108
|
+
return output;
|
|
109
|
+
}
|
|
110
|
+
function registerUpgradeCheckCommand(command, dependencies = {}) {
|
|
111
|
+
return command
|
|
112
|
+
.command('upgrade-check')
|
|
113
|
+
.description('Check release safety for an upgrade using the public release index')
|
|
114
|
+
.requiredOption('--from <version>', 'Current Lightdash version')
|
|
115
|
+
.requiredOption('--to <version>', 'Target Lightdash version')
|
|
116
|
+
.option('--json', 'Print machine-readable JSON', false)
|
|
117
|
+
.action(async (options) => {
|
|
118
|
+
await upgradeCheckHandler(options, dependencies);
|
|
119
|
+
});
|
|
120
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"upgradeCheck.test.d.ts","sourceRoot":"","sources":["../../src/handlers/upgradeCheck.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const commander_1 = require("commander");
|
|
4
|
+
const node_fetch_1 = require("node-fetch");
|
|
5
|
+
const releaseSafety_1 = require("../releaseSafety");
|
|
6
|
+
const upgradeCheck_1 = require("./upgradeCheck");
|
|
7
|
+
const safeIndex = {
|
|
8
|
+
schemaVersion: releaseSafety_1.INDEX_SCHEMA_VERSION,
|
|
9
|
+
generatedAt: '2026-08-11T00:00:00.000Z',
|
|
10
|
+
backfillFloorVersion: null,
|
|
11
|
+
entries: [
|
|
12
|
+
{
|
|
13
|
+
version: '1.114.0',
|
|
14
|
+
previousVersion: '1.113.0',
|
|
15
|
+
releaseDate: '2026-07-31T00:00:00.000Z',
|
|
16
|
+
rollingUpdateSafe: true,
|
|
17
|
+
requiredStops: [],
|
|
18
|
+
minPreviousVersion: null,
|
|
19
|
+
backfilled: true,
|
|
20
|
+
syntheticRequiredStop: false,
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
version: '1.115.0',
|
|
24
|
+
previousVersion: '1.114.0',
|
|
25
|
+
releaseDate: '2026-08-01T00:00:00.000Z',
|
|
26
|
+
rollingUpdateSafe: true,
|
|
27
|
+
requiredStops: [],
|
|
28
|
+
minPreviousVersion: null,
|
|
29
|
+
backfilled: true,
|
|
30
|
+
syntheticRequiredStop: false,
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
};
|
|
34
|
+
class ExitError extends Error {
|
|
35
|
+
code;
|
|
36
|
+
constructor(code) {
|
|
37
|
+
super(`exit ${code}`);
|
|
38
|
+
this.code = code;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function responseFor(index) {
|
|
42
|
+
return new node_fetch_1.Response(JSON.stringify(index), {
|
|
43
|
+
status: 200,
|
|
44
|
+
headers: { 'content-type': 'application/json' },
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
function dependenciesFor(index) {
|
|
48
|
+
const fetchIndex = vi
|
|
49
|
+
.fn()
|
|
50
|
+
.mockResolvedValue(responseFor(index));
|
|
51
|
+
const output = [];
|
|
52
|
+
return {
|
|
53
|
+
dependencies: {
|
|
54
|
+
fetch: fetchIndex,
|
|
55
|
+
writeOutput: (message) => output.push(message),
|
|
56
|
+
exit: (code) => {
|
|
57
|
+
throw new ExitError(code);
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
fetchIndex,
|
|
61
|
+
output,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
describe('upgradeCheckHandler', () => {
|
|
65
|
+
it('fetches the public index without authentication context', async () => {
|
|
66
|
+
const { dependencies, fetchIndex } = dependenciesFor(safeIndex);
|
|
67
|
+
await (0, upgradeCheck_1.upgradeCheckHandler)({ from: '1.114.0', to: '1.115.0' }, dependencies);
|
|
68
|
+
expect(fetchIndex).toHaveBeenCalledTimes(1);
|
|
69
|
+
expect(fetchIndex).toHaveBeenCalledWith(upgradeCheck_1.RELEASE_SAFETY_INDEX_URL);
|
|
70
|
+
expect(fetchIndex.mock.calls[0]).toHaveLength(1);
|
|
71
|
+
});
|
|
72
|
+
it('renders a boundary-precise gap and exits non-zero', async () => {
|
|
73
|
+
const gapIndex = {
|
|
74
|
+
...safeIndex,
|
|
75
|
+
entries: [
|
|
76
|
+
...safeIndex.entries,
|
|
77
|
+
{
|
|
78
|
+
...safeIndex.entries[0],
|
|
79
|
+
version: '1.121.0',
|
|
80
|
+
previousVersion: '1.120.1',
|
|
81
|
+
releaseDate: '2026-08-07T00:00:00.000Z',
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
};
|
|
85
|
+
const { dependencies, output } = dependenciesFor(gapIndex);
|
|
86
|
+
await expect((0, upgradeCheck_1.upgradeCheckHandler)({ from: '1.114.0', to: '1.121.0' }, dependencies)).rejects.toMatchObject({ code: 1 });
|
|
87
|
+
expect(output.join('\n')).toContain('after 1.115.0 and before 1.121.0');
|
|
88
|
+
expect(output.join('\n')).toContain('Verdict: UNSAFE');
|
|
89
|
+
});
|
|
90
|
+
it('fails closed when the source version is not indexed', async () => {
|
|
91
|
+
const indexWithMissingSource = {
|
|
92
|
+
...safeIndex,
|
|
93
|
+
entries: safeIndex.entries.filter((entry) => entry.version !== '1.114.0'),
|
|
94
|
+
};
|
|
95
|
+
const { dependencies, output } = dependenciesFor(indexWithMissingSource);
|
|
96
|
+
await expect((0, upgradeCheck_1.upgradeCheckHandler)({ from: '1.114.0', to: '1.115.0' }, dependencies)).rejects.toMatchObject({ code: 1 });
|
|
97
|
+
expect(output[0]).toContain('version 1.114.0 is not present in the release-safety index');
|
|
98
|
+
});
|
|
99
|
+
it('emits the stable JSON shape without human output', async () => {
|
|
100
|
+
const { dependencies, output } = dependenciesFor(safeIndex);
|
|
101
|
+
await (0, upgradeCheck_1.upgradeCheckHandler)({ from: '1.114.0', to: '1.115.0', json: true }, dependencies);
|
|
102
|
+
expect(JSON.parse(output[0])).toEqual({
|
|
103
|
+
fromVersion: '1.114.0',
|
|
104
|
+
toVersion: '1.115.0',
|
|
105
|
+
direction: 'upgrade',
|
|
106
|
+
safe: true,
|
|
107
|
+
verdict: true,
|
|
108
|
+
requiredStops: [],
|
|
109
|
+
minPreviousVersion: null,
|
|
110
|
+
coveredVersions: ['1.115.0'],
|
|
111
|
+
missingRanges: [],
|
|
112
|
+
});
|
|
113
|
+
expect(output).toHaveLength(1);
|
|
114
|
+
});
|
|
115
|
+
it('preserves same-version evaluation', async () => {
|
|
116
|
+
const { dependencies, output } = dependenciesFor(safeIndex);
|
|
117
|
+
await (0, upgradeCheck_1.upgradeCheckHandler)({ from: '1.115.0', to: '1.115.0', json: true }, dependencies);
|
|
118
|
+
expect(JSON.parse(output[0])).toMatchObject({
|
|
119
|
+
direction: 'same-version',
|
|
120
|
+
safe: true,
|
|
121
|
+
verdict: true,
|
|
122
|
+
coveredVersions: [],
|
|
123
|
+
missingRanges: [],
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
it('names false and unknown release reasons in human output', async () => {
|
|
127
|
+
const unsafeIndex = {
|
|
128
|
+
...safeIndex,
|
|
129
|
+
entries: [
|
|
130
|
+
safeIndex.entries[0],
|
|
131
|
+
{
|
|
132
|
+
...safeIndex.entries[1],
|
|
133
|
+
rollingUpdateSafe: false,
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
...safeIndex.entries[1],
|
|
137
|
+
version: '1.116.0',
|
|
138
|
+
previousVersion: '1.115.0',
|
|
139
|
+
rollingUpdateSafe: 'unknown',
|
|
140
|
+
},
|
|
141
|
+
],
|
|
142
|
+
};
|
|
143
|
+
const { dependencies, output } = dependenciesFor(unsafeIndex);
|
|
144
|
+
await expect((0, upgradeCheck_1.upgradeCheckHandler)({ from: '1.114.0', to: '1.116.0' }, dependencies)).rejects.toMatchObject({ code: 1 });
|
|
145
|
+
expect(output[0]).toContain('Unsafe rolling-update safety: 1.115.0');
|
|
146
|
+
expect(output[0]).toContain('Unknown rolling-update safety: 1.116.0');
|
|
147
|
+
});
|
|
148
|
+
it('rejects non-X.Y.Z endpoints before fetching', async () => {
|
|
149
|
+
const { dependencies, fetchIndex } = dependenciesFor(safeIndex);
|
|
150
|
+
await expect((0, upgradeCheck_1.upgradeCheckHandler)({ from: 'v1.114.0', to: '1.115.0' }, dependencies)).rejects.toThrow('--from must be a release version in X.Y.Z format');
|
|
151
|
+
expect(fetchIndex).not.toHaveBeenCalled();
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
describe('upgrade-check argv and exit codes', () => {
|
|
155
|
+
async function run(argv, index = safeIndex) {
|
|
156
|
+
const { dependencies } = dependenciesFor(index);
|
|
157
|
+
const command = new commander_1.Command()
|
|
158
|
+
.name('lightdash')
|
|
159
|
+
.exitOverride()
|
|
160
|
+
.configureOutput({ writeOut: () => { }, writeErr: () => { } });
|
|
161
|
+
(0, upgradeCheck_1.registerUpgradeCheckCommand)(command, dependencies);
|
|
162
|
+
try {
|
|
163
|
+
await command.parseAsync(['node', 'lightdash', ...argv]);
|
|
164
|
+
return 0;
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
if (error instanceof ExitError) {
|
|
168
|
+
return error.code;
|
|
169
|
+
}
|
|
170
|
+
if (error instanceof commander_1.CommanderError) {
|
|
171
|
+
return error.exitCode;
|
|
172
|
+
}
|
|
173
|
+
throw error;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
it.each([
|
|
177
|
+
{
|
|
178
|
+
name: 'green forward span',
|
|
179
|
+
argv: ['upgrade-check', '--from', '1.114.0', '--to', '1.115.0'],
|
|
180
|
+
expected: 0,
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
name: 'unsafe missing target',
|
|
184
|
+
argv: ['upgrade-check', '--from', '1.114.0', '--to', '1.116.0'],
|
|
185
|
+
expected: 1,
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
name: 'missing from option',
|
|
189
|
+
argv: ['upgrade-check', '--to', '1.115.0'],
|
|
190
|
+
expected: 1,
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
name: 'missing to option',
|
|
194
|
+
argv: ['upgrade-check', '--from', '1.114.0'],
|
|
195
|
+
expected: 1,
|
|
196
|
+
},
|
|
197
|
+
])('returns $expected for $name', async ({ argv, expected }) => {
|
|
198
|
+
await expect(run(argv)).resolves.toBe(expected);
|
|
199
|
+
});
|
|
200
|
+
it('rejects a reverse argv span with a non-zero exit and rollback guidance', async () => {
|
|
201
|
+
const { dependencies, fetchIndex, output } = dependenciesFor(safeIndex);
|
|
202
|
+
const command = new commander_1.Command().name('lightdash').exitOverride();
|
|
203
|
+
(0, upgradeCheck_1.registerUpgradeCheckCommand)(command, dependencies);
|
|
204
|
+
let exitCode = 0;
|
|
205
|
+
let errorMessage = '';
|
|
206
|
+
try {
|
|
207
|
+
await command.parseAsync([
|
|
208
|
+
'node',
|
|
209
|
+
'lightdash',
|
|
210
|
+
'upgrade-check',
|
|
211
|
+
'--from',
|
|
212
|
+
'1.115.0',
|
|
213
|
+
'--to',
|
|
214
|
+
'1.114.0',
|
|
215
|
+
'--json',
|
|
216
|
+
]);
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
exitCode = error instanceof ExitError ? error.code : 1;
|
|
220
|
+
errorMessage = error instanceof Error ? error.message : '';
|
|
221
|
+
}
|
|
222
|
+
expect(exitCode).toBe(1);
|
|
223
|
+
expect(errorMessage).toBe('Rollback spans are not supported by this command; rollback guidance ships in the upgrade runbook.');
|
|
224
|
+
expect(fetchIndex).not.toHaveBeenCalled();
|
|
225
|
+
expect(output).toHaveLength(0);
|
|
226
|
+
});
|
|
227
|
+
});
|
package/dist/index.js
CHANGED
|
@@ -34,6 +34,7 @@ const runChart_1 = require("./handlers/runChart");
|
|
|
34
34
|
const setProject_1 = require("./handlers/setProject");
|
|
35
35
|
const setWarehouse_1 = require("./handlers/setWarehouse");
|
|
36
36
|
const sql_1 = require("./handlers/sql");
|
|
37
|
+
const upgradeCheck_1 = require("./handlers/upgradeCheck");
|
|
37
38
|
const validate_2 = require("./handlers/validate");
|
|
38
39
|
const warehouseCatalog_1 = require("./handlers/warehouseCatalog");
|
|
39
40
|
const styles = tslib_1.__importStar(require("./styles"));
|
|
@@ -104,6 +105,7 @@ ${styles.bold('Examples:')}
|
|
|
104
105
|
${styles.title('⚡')}️lightdash ${styles.bold('deploy')} ${styles.secondary('-- compiles and deploys Lightdash metrics to active project')}
|
|
105
106
|
${styles.title('⚡')}️lightdash ${styles.bold('login https://lightdash.domain.com')} ${styles.secondary('-- logs in to a Lightdash instance')}
|
|
106
107
|
`);
|
|
108
|
+
(0, upgradeCheck_1.registerUpgradeCheckCommand)(commander_1.program);
|
|
107
109
|
commander_1.program
|
|
108
110
|
.command('connect-snowflake')
|
|
109
111
|
.description('Connects Lightdash to Snowflake using browser-based SSO')
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export { compareVersions, isReleaseVersion } from './version';
|
|
2
|
+
export declare const INDEX_SCHEMA_VERSION: '1';
|
|
3
|
+
export type TriState = boolean | 'unknown';
|
|
4
|
+
export interface ReleaseSafetyIndexEntry {
|
|
5
|
+
version: string;
|
|
6
|
+
previousVersion: string | null;
|
|
7
|
+
releaseDate: string;
|
|
8
|
+
rollingUpdateSafe: TriState;
|
|
9
|
+
requiredStops: string[];
|
|
10
|
+
minPreviousVersion: string | null;
|
|
11
|
+
backfilled: boolean;
|
|
12
|
+
syntheticRequiredStop: boolean;
|
|
13
|
+
}
|
|
14
|
+
export interface ReleaseSafetyIndex {
|
|
15
|
+
schemaVersion: typeof INDEX_SCHEMA_VERSION;
|
|
16
|
+
generatedAt: string;
|
|
17
|
+
backfillFloorVersion: string | null;
|
|
18
|
+
entries: ReleaseSafetyIndexEntry[];
|
|
19
|
+
}
|
|
20
|
+
export interface ReleaseSafetyMissingRange {
|
|
21
|
+
afterVersion: string;
|
|
22
|
+
beforeVersion: string;
|
|
23
|
+
}
|
|
24
|
+
export interface SpanComposition {
|
|
25
|
+
verdict: TriState;
|
|
26
|
+
safe: boolean;
|
|
27
|
+
requiredStops: string[];
|
|
28
|
+
minPreviousVersion: string | null;
|
|
29
|
+
coveredVersions: string[];
|
|
30
|
+
missingRanges: ReleaseSafetyMissingRange[];
|
|
31
|
+
}
|
|
32
|
+
export declare function parseReleaseSafetyIndex(input: unknown): ReleaseSafetyIndex;
|
|
33
|
+
export declare function composeReleaseSafetySpan(index: ReleaseSafetyIndex, fromVersion: string, toVersion: string): SpanComposition;
|
|
34
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/releaseSafety/index.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAE9D,eAAO,MAAM,oBAAoB,EAAG,GAAY,CAAC;AAEjD,MAAM,MAAM,QAAQ,GAAG,OAAO,GAAG,SAAS,CAAC;AAE3C,MAAM,WAAW,uBAAuB;IACpC,OAAO,EAAE,MAAM,CAAC;IAChB,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,WAAW,EAAE,MAAM,CAAC;IACpB,iBAAiB,EAAE,QAAQ,CAAC;IAC5B,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,UAAU,EAAE,OAAO,CAAC;IACpB,qBAAqB,EAAE,OAAO,CAAC;CAClC;AAED,MAAM,WAAW,kBAAkB;IAC/B,aAAa,EAAE,OAAO,oBAAoB,CAAC;IAC3C,WAAW,EAAE,MAAM,CAAC;IACpB,oBAAoB,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC,OAAO,EAAE,uBAAuB,EAAE,CAAC;CACtC;AAED,MAAM,WAAW,yBAAyB;IACtC,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,eAAe;IAC5B,OAAO,EAAE,QAAQ,CAAC;IAClB,IAAI,EAAE,OAAO,CAAC;IACd,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,aAAa,EAAE,yBAAyB,EAAE,CAAC;CAC9C;AAcD,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,GAAG,kBAAkB,CAO1E;AAED,wBAAgB,wBAAwB,CACpC,KAAK,EAAE,kBAAkB,EACzB,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,GAClB,eAAe,CA4GjB"}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.INDEX_SCHEMA_VERSION = exports.isReleaseVersion = exports.compareVersions = void 0;
|
|
4
|
+
exports.parseReleaseSafetyIndex = parseReleaseSafetyIndex;
|
|
5
|
+
exports.composeReleaseSafetySpan = composeReleaseSafetySpan;
|
|
6
|
+
const tslib_1 = require("tslib");
|
|
7
|
+
const ajv_1 = tslib_1.__importDefault(require("ajv"));
|
|
8
|
+
const ajv_formats_1 = tslib_1.__importDefault(require("ajv-formats"));
|
|
9
|
+
const release_safety_index_schema_json_1 = tslib_1.__importDefault(require("./release-safety-index.schema.json"));
|
|
10
|
+
const version_1 = require("./version");
|
|
11
|
+
var version_2 = require("./version");
|
|
12
|
+
Object.defineProperty(exports, "compareVersions", { enumerable: true, get: function () { return version_2.compareVersions; } });
|
|
13
|
+
Object.defineProperty(exports, "isReleaseVersion", { enumerable: true, get: function () { return version_2.isReleaseVersion; } });
|
|
14
|
+
exports.INDEX_SCHEMA_VERSION = '1';
|
|
15
|
+
const ajv = new ajv_1.default({
|
|
16
|
+
strict: true,
|
|
17
|
+
allErrors: true,
|
|
18
|
+
coerceTypes: false,
|
|
19
|
+
removeAdditional: false,
|
|
20
|
+
useDefaults: false,
|
|
21
|
+
});
|
|
22
|
+
(0, ajv_formats_1.default)(ajv);
|
|
23
|
+
const validateReleaseSafetyIndex = ajv.compile(release_safety_index_schema_json_1.default);
|
|
24
|
+
function parseReleaseSafetyIndex(input) {
|
|
25
|
+
if (!validateReleaseSafetyIndex(input)) {
|
|
26
|
+
throw new Error(`Invalid release-safety index: ${JSON.stringify(validateReleaseSafetyIndex.errors)}`);
|
|
27
|
+
}
|
|
28
|
+
return input;
|
|
29
|
+
}
|
|
30
|
+
function composeReleaseSafetySpan(index, fromVersion, toVersion) {
|
|
31
|
+
const [lowerVersion, upperVersion] = (0, version_1.compareVersions)(fromVersion, toVersion) <= 0
|
|
32
|
+
? [fromVersion, toVersion]
|
|
33
|
+
: [toVersion, fromVersion];
|
|
34
|
+
const entries = index.entries
|
|
35
|
+
.filter((entry) => (0, version_1.compareVersions)(entry.version, lowerVersion) > 0 &&
|
|
36
|
+
(0, version_1.compareVersions)(entry.version, upperVersion) <= 0)
|
|
37
|
+
.sort((left, right) => (0, version_1.compareVersions)(left.version, right.version));
|
|
38
|
+
const indexedVersions = new Set(index.entries.map((entry) => entry.version));
|
|
39
|
+
const missingRanges = [];
|
|
40
|
+
if (!indexedVersions.has(lowerVersion)) {
|
|
41
|
+
missingRanges.push({
|
|
42
|
+
afterVersion: lowerVersion,
|
|
43
|
+
beforeVersion: lowerVersion,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
let expectedPreviousVersion = lowerVersion;
|
|
47
|
+
for (const entry of entries) {
|
|
48
|
+
if (entry.previousVersion === null ||
|
|
49
|
+
(0, version_1.compareVersions)(entry.previousVersion, expectedPreviousVersion) !==
|
|
50
|
+
0) {
|
|
51
|
+
missingRanges.push({
|
|
52
|
+
afterVersion: expectedPreviousVersion,
|
|
53
|
+
beforeVersion: entry.version,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
expectedPreviousVersion = entry.version;
|
|
57
|
+
}
|
|
58
|
+
if ((0, version_1.compareVersions)(expectedPreviousVersion, upperVersion) !== 0) {
|
|
59
|
+
const terminalRange = {
|
|
60
|
+
afterVersion: expectedPreviousVersion,
|
|
61
|
+
beforeVersion: upperVersion,
|
|
62
|
+
};
|
|
63
|
+
if (!missingRanges.some((range) => range.afterVersion === terminalRange.afterVersion &&
|
|
64
|
+
range.beforeVersion === terminalRange.beforeVersion)) {
|
|
65
|
+
missingRanges.push(terminalRange);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const startsBeforeFloor = index.backfillFloorVersion !== null &&
|
|
69
|
+
(0, version_1.compareVersions)(lowerVersion, index.backfillFloorVersion) < 0;
|
|
70
|
+
const hasFalse = entries.some((entry) => entry.rollingUpdateSafe === false);
|
|
71
|
+
const hasUnknown = entries.some((entry) => entry.rollingUpdateSafe === 'unknown');
|
|
72
|
+
let verdict = true;
|
|
73
|
+
if (missingRanges.length > 0) {
|
|
74
|
+
verdict = 'unknown';
|
|
75
|
+
}
|
|
76
|
+
else if (hasFalse) {
|
|
77
|
+
verdict = false;
|
|
78
|
+
}
|
|
79
|
+
else if (hasUnknown) {
|
|
80
|
+
verdict = 'unknown';
|
|
81
|
+
}
|
|
82
|
+
const requiredStops = [
|
|
83
|
+
...new Set([
|
|
84
|
+
...(startsBeforeFloor && index.backfillFloorVersion !== null
|
|
85
|
+
? [index.backfillFloorVersion]
|
|
86
|
+
: []),
|
|
87
|
+
...entries.flatMap((entry) => entry.requiredStops),
|
|
88
|
+
]),
|
|
89
|
+
].filter((version) => (0, version_1.compareVersions)(version, lowerVersion) > 0 &&
|
|
90
|
+
(0, version_1.compareVersions)(version, upperVersion) <= 0);
|
|
91
|
+
const minPreviousVersion = entries.reduce((highest, entry) => {
|
|
92
|
+
if (entry.minPreviousVersion === null) {
|
|
93
|
+
return highest;
|
|
94
|
+
}
|
|
95
|
+
if (highest === null ||
|
|
96
|
+
(0, version_1.compareVersions)(entry.minPreviousVersion, highest) > 0) {
|
|
97
|
+
return entry.minPreviousVersion;
|
|
98
|
+
}
|
|
99
|
+
return highest;
|
|
100
|
+
}, null);
|
|
101
|
+
const belowMinimum = minPreviousVersion !== null &&
|
|
102
|
+
(0, version_1.compareVersions)(lowerVersion, minPreviousVersion) < 0;
|
|
103
|
+
return {
|
|
104
|
+
verdict,
|
|
105
|
+
safe: verdict === true && requiredStops.length === 0 && !belowMinimum,
|
|
106
|
+
requiredStops,
|
|
107
|
+
minPreviousVersion,
|
|
108
|
+
coveredVersions: entries.map((entry) => entry.version),
|
|
109
|
+
missingRanges,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.test.d.ts","sourceRoot":"","sources":["../../src/releaseSafety/index.test.ts"],"names":[],"mappings":""}
|