@netlify/build 35.11.2 → 35.12.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.
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
export declare const MIGRATION_DIR_PATTERN: RegExp;
|
|
2
2
|
export declare const MIGRATION_FILE_PATTERN: RegExp;
|
|
3
|
-
interface
|
|
3
|
+
interface MissingSqlFileError {
|
|
4
4
|
type: 'missing_sql_file';
|
|
5
5
|
dirName: string;
|
|
6
6
|
}
|
|
7
|
+
interface DuplicateMigrationNumberError {
|
|
8
|
+
type: 'duplicate_migration_number';
|
|
9
|
+
migrationNumber: string;
|
|
10
|
+
names: string[];
|
|
11
|
+
}
|
|
12
|
+
type ValidationError = MissingSqlFileError | DuplicateMigrationNumberError;
|
|
7
13
|
interface ValidationResult {
|
|
8
14
|
valid: true;
|
|
9
15
|
dirs: string[];
|
|
@@ -13,6 +19,7 @@ interface ValidationFailure {
|
|
|
13
19
|
valid: false;
|
|
14
20
|
errors: ValidationError[];
|
|
15
21
|
}
|
|
22
|
+
export declare const trackMigrationNumber: (numberToNames: Map<string, string[]>, name: string) => void;
|
|
16
23
|
export declare const validateMigrations: (dirNames: string[], fileNames: string[], existingSqlFiles: Set<string>) => ValidationResult | ValidationFailure;
|
|
17
24
|
export declare const formatValidationErrors: (errors: ValidationError[]) => string;
|
|
18
25
|
export {};
|
|
@@ -1,35 +1,56 @@
|
|
|
1
1
|
export const MIGRATION_DIR_PATTERN = /^\d+_[a-z0-9_-]+$/;
|
|
2
2
|
export const MIGRATION_FILE_PATTERN = /^\d+_[a-z0-9_-]+\.sql$/;
|
|
3
|
+
export const trackMigrationNumber = (numberToNames, name) => {
|
|
4
|
+
const key = /^(\d+)_/.exec(name)[1].replace(/^0+/, '') || '0';
|
|
5
|
+
const existing = numberToNames.get(key);
|
|
6
|
+
if (existing) {
|
|
7
|
+
existing.push(name);
|
|
8
|
+
}
|
|
9
|
+
else {
|
|
10
|
+
numberToNames.set(key, [name]);
|
|
11
|
+
}
|
|
12
|
+
};
|
|
3
13
|
export const validateMigrations = (dirNames, fileNames, existingSqlFiles) => {
|
|
4
14
|
const errors = [];
|
|
5
15
|
const matchingDirs = [];
|
|
16
|
+
const numberToNames = new Map();
|
|
6
17
|
for (const dirName of dirNames) {
|
|
7
18
|
if (!MIGRATION_DIR_PATTERN.test(dirName)) {
|
|
8
19
|
continue;
|
|
9
20
|
}
|
|
10
21
|
matchingDirs.push(dirName);
|
|
22
|
+
trackMigrationNumber(numberToNames, dirName);
|
|
11
23
|
if (!existingSqlFiles.has(dirName)) {
|
|
12
24
|
errors.push({ type: 'missing_sql_file', dirName });
|
|
13
25
|
}
|
|
14
26
|
}
|
|
15
|
-
if (errors.length > 0) {
|
|
16
|
-
return { valid: false, errors };
|
|
17
|
-
}
|
|
18
27
|
const matchingFiles = [];
|
|
19
28
|
for (const fileName of fileNames) {
|
|
20
29
|
if (MIGRATION_FILE_PATTERN.test(fileName)) {
|
|
21
30
|
matchingFiles.push(fileName);
|
|
31
|
+
trackMigrationNumber(numberToNames, fileName);
|
|
22
32
|
}
|
|
23
33
|
}
|
|
34
|
+
for (const [migrationNumber, names] of numberToNames) {
|
|
35
|
+
if (names.length > 1) {
|
|
36
|
+
errors.push({ type: 'duplicate_migration_number', migrationNumber, names: names.sort() });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (errors.length > 0) {
|
|
40
|
+
return { valid: false, errors };
|
|
41
|
+
}
|
|
24
42
|
return {
|
|
25
43
|
valid: true,
|
|
26
|
-
dirs:
|
|
27
|
-
files:
|
|
44
|
+
dirs: matchingDirs.sort(),
|
|
45
|
+
files: matchingFiles.sort(),
|
|
28
46
|
};
|
|
29
47
|
};
|
|
30
48
|
export const formatValidationErrors = (errors) => {
|
|
31
49
|
const lines = errors.map((error) => {
|
|
32
|
-
|
|
50
|
+
if (error.type === 'missing_sql_file') {
|
|
51
|
+
return ` - "${error.dirName}/migration.sql" is missing.`;
|
|
52
|
+
}
|
|
53
|
+
return ` - Duplicate migration number ${error.migrationNumber}: ${error.names.map((n) => `"${n}"`).join(', ')}`;
|
|
33
54
|
});
|
|
34
55
|
return `Database migration validation failed:\n${lines.join('\n')}`;
|
|
35
56
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, test } from 'vitest';
|
|
2
|
-
import { MIGRATION_DIR_PATTERN, MIGRATION_FILE_PATTERN, validateMigrations, formatValidationErrors, } from './validation.js';
|
|
2
|
+
import { MIGRATION_DIR_PATTERN, MIGRATION_FILE_PATTERN, trackMigrationNumber, validateMigrations, formatValidationErrors, } from './validation.js';
|
|
3
3
|
describe('MIGRATION_DIR_PATTERN', () => {
|
|
4
4
|
const validNames = [
|
|
5
5
|
'1700000000_create-users',
|
|
@@ -55,6 +55,29 @@ describe('MIGRATION_FILE_PATTERN', () => {
|
|
|
55
55
|
expect(MIGRATION_FILE_PATTERN.test(name)).toBe(false);
|
|
56
56
|
});
|
|
57
57
|
});
|
|
58
|
+
describe('trackMigrationNumber', () => {
|
|
59
|
+
const cases = [
|
|
60
|
+
{ name: '001_create-users', expected: '1' },
|
|
61
|
+
{ name: '1_init', expected: '1' },
|
|
62
|
+
{ name: '0001_add-posts', expected: '1' },
|
|
63
|
+
{ name: '42_z', expected: '42' },
|
|
64
|
+
{ name: '1700000000_create-users', expected: '1700000000' },
|
|
65
|
+
{ name: '1700000000_create-users.sql', expected: '1700000000' },
|
|
66
|
+
{ name: '001_create-users.sql', expected: '1' },
|
|
67
|
+
{ name: '0000000000_init', expected: '0' },
|
|
68
|
+
];
|
|
69
|
+
test.each(cases)('tracks $name under key $expected', ({ name, expected }) => {
|
|
70
|
+
const map = new Map();
|
|
71
|
+
trackMigrationNumber(map, name);
|
|
72
|
+
expect(map.get(expected)).toEqual([name]);
|
|
73
|
+
});
|
|
74
|
+
test('groups names with the same migration number', () => {
|
|
75
|
+
const map = new Map();
|
|
76
|
+
trackMigrationNumber(map, '001_create-users');
|
|
77
|
+
trackMigrationNumber(map, '1_init');
|
|
78
|
+
expect(map.get('1')).toEqual(['001_create-users', '1_init']);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
58
81
|
describe('validateMigrations', () => {
|
|
59
82
|
test('returns valid result with sorted dirs when all dirs are valid', () => {
|
|
60
83
|
const dirNames = ['1700000001_add-posts', '1700000000_create-users'];
|
|
@@ -132,6 +155,52 @@ describe('validateMigrations', () => {
|
|
|
132
155
|
files: [],
|
|
133
156
|
});
|
|
134
157
|
});
|
|
158
|
+
test('returns error for duplicate migration numbers in dirs', () => {
|
|
159
|
+
const dirNames = ['001_create-users', '001_add-posts'];
|
|
160
|
+
const existingSqlFiles = new Set(['001_create-users', '001_add-posts']);
|
|
161
|
+
const result = validateMigrations(dirNames, [], existingSqlFiles);
|
|
162
|
+
expect(result).toEqual({
|
|
163
|
+
valid: false,
|
|
164
|
+
errors: [
|
|
165
|
+
{ type: 'duplicate_migration_number', migrationNumber: '1', names: ['001_add-posts', '001_create-users'] },
|
|
166
|
+
],
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
test('returns error for duplicate migration numbers in files', () => {
|
|
170
|
+
const fileNames = ['001_create-users.sql', '001_add-posts.sql'];
|
|
171
|
+
const result = validateMigrations([], fileNames, new Set());
|
|
172
|
+
expect(result).toEqual({
|
|
173
|
+
valid: false,
|
|
174
|
+
errors: [
|
|
175
|
+
{
|
|
176
|
+
type: 'duplicate_migration_number',
|
|
177
|
+
migrationNumber: '1',
|
|
178
|
+
names: ['001_add-posts.sql', '001_create-users.sql'],
|
|
179
|
+
},
|
|
180
|
+
],
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
test('returns error for duplicate migration numbers across dirs and files', () => {
|
|
184
|
+
const dirNames = ['001_create-users'];
|
|
185
|
+
const fileNames = ['001_add-posts.sql'];
|
|
186
|
+
const existingSqlFiles = new Set(['001_create-users']);
|
|
187
|
+
const result = validateMigrations(dirNames, fileNames, existingSqlFiles);
|
|
188
|
+
expect(result).toEqual({
|
|
189
|
+
valid: false,
|
|
190
|
+
errors: [
|
|
191
|
+
{ type: 'duplicate_migration_number', migrationNumber: '1', names: ['001_add-posts.sql', '001_create-users'] },
|
|
192
|
+
],
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
test('treats different zero-padded prefixes as duplicates', () => {
|
|
196
|
+
const dirNames = ['1_init', '001_setup'];
|
|
197
|
+
const existingSqlFiles = new Set(['1_init', '001_setup']);
|
|
198
|
+
const result = validateMigrations(dirNames, [], existingSqlFiles);
|
|
199
|
+
expect(result).toEqual({
|
|
200
|
+
valid: false,
|
|
201
|
+
errors: [{ type: 'duplicate_migration_number', migrationNumber: '1', names: ['001_setup', '1_init'] }],
|
|
202
|
+
});
|
|
203
|
+
});
|
|
135
204
|
});
|
|
136
205
|
describe('formatValidationErrors', () => {
|
|
137
206
|
test('formats missing_sql_file errors', () => {
|
|
@@ -147,4 +216,11 @@ describe('formatValidationErrors', () => {
|
|
|
147
216
|
expect(message).toContain('"1700000000_create-users/migration.sql" is missing');
|
|
148
217
|
expect(message).toContain('"1700000001_add-posts/migration.sql" is missing');
|
|
149
218
|
});
|
|
219
|
+
test('formats duplicate_migration_number errors', () => {
|
|
220
|
+
const message = formatValidationErrors([
|
|
221
|
+
{ type: 'duplicate_migration_number', migrationNumber: '1', names: ['001_add-posts', '001_create-users'] },
|
|
222
|
+
]);
|
|
223
|
+
expect(message).toContain('Database migration validation failed');
|
|
224
|
+
expect(message).toContain('Duplicate migration number 1: "001_add-posts", "001_create-users"');
|
|
225
|
+
});
|
|
150
226
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createReadStream, promises as fs, existsSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { fdir } from 'fdir';
|
|
4
|
+
import ignore from 'ignore';
|
|
4
5
|
import { minimatch } from 'minimatch';
|
|
5
6
|
import { LIKELY_SECRET_PREFIXES, SAFE_LISTED_VALUES } from './secret_prefixes.js';
|
|
6
7
|
/**
|
|
@@ -170,21 +171,16 @@ export function findLikelySecrets({ text, omitValuesFromEnhancedScan = [], }) {
|
|
|
170
171
|
*/
|
|
171
172
|
export async function getFilePathsToScan({ env, base }) {
|
|
172
173
|
const omitPathsAlways = ['.git/', '.cache/'];
|
|
173
|
-
//
|
|
174
|
-
//
|
|
175
|
-
//
|
|
176
|
-
// node_modules anywhere we will omit looking in those folders - this will allow repos
|
|
177
|
-
// that do commit node_modules to still scan them.
|
|
178
|
-
let ignoreNodeModules = false;
|
|
174
|
+
// Files/folders ignored by the repo's .gitignore should not be scanned, since they
|
|
175
|
+
// are not committed to the repo and are not part of the build output the user controls.
|
|
176
|
+
// This also naturally excludes things like node_modules when the repo gitignores it.
|
|
179
177
|
const gitignorePath = path.resolve(base, '.gitignore');
|
|
180
178
|
const gitignoreContents = existsSync(gitignorePath) ? await fs.readFile(gitignorePath, 'utf-8') : '';
|
|
181
|
-
|
|
182
|
-
ignoreNodeModules = true;
|
|
183
|
-
}
|
|
179
|
+
const gitIgnoreFilter = gitignoreContents ? ignore().add(gitignoreContents) : null;
|
|
184
180
|
let files = await new fdir()
|
|
185
181
|
.withRelativePaths()
|
|
186
|
-
.filter((
|
|
187
|
-
if (
|
|
182
|
+
.filter((filePath) => {
|
|
183
|
+
if (gitIgnoreFilter && filePath && gitIgnoreFilter.ignores(filePath)) {
|
|
188
184
|
return false;
|
|
189
185
|
}
|
|
190
186
|
return true;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@netlify/build",
|
|
3
|
-
"version": "35.
|
|
3
|
+
"version": "35.12.0",
|
|
4
4
|
"description": "Netlify build module",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": "./lib/index.js",
|
|
@@ -86,6 +86,7 @@
|
|
|
86
86
|
"figures": "^6.0.0",
|
|
87
87
|
"filter-obj": "^6.0.0",
|
|
88
88
|
"hot-shots": "11.4.0",
|
|
89
|
+
"ignore": "^7.0.0",
|
|
89
90
|
"indent-string": "^5.0.0",
|
|
90
91
|
"is-plain-obj": "^4.0.0",
|
|
91
92
|
"keep-func-props": "^6.0.0",
|
|
@@ -152,5 +153,5 @@
|
|
|
152
153
|
"engines": {
|
|
153
154
|
"node": ">=18.14.0"
|
|
154
155
|
},
|
|
155
|
-
"gitHead": "
|
|
156
|
+
"gitHead": "85ec413dbce5342df01289953b91169c1f84c34b"
|
|
156
157
|
}
|