@netlify/build 35.11.1 → 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 ValidationError {
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: [...matchingDirs].sort(),
27
- files: [...matchingFiles].sort(),
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
- return ` - "${error.dirName}/migration.sql" is missing.`;
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
- // node modules is dense and is only useful to scan if the repo itself commits these
174
- // files. As a simple check to understand if the repo would commit these files, we expect
175
- // that they would not ignore them from their git settings. So if gitignore includes
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
- if (gitignoreContents?.includes('node_modules')) {
182
- ignoreNodeModules = true;
183
- }
179
+ const gitIgnoreFilter = gitignoreContents ? ignore().add(gitignoreContents) : null;
184
180
  let files = await new fdir()
185
181
  .withRelativePaths()
186
- .filter((path) => {
187
- if (ignoreNodeModules && path.includes('node_modules')) {
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.11.1",
3
+ "version": "35.12.0",
4
4
  "description": "Netlify build module",
5
5
  "type": "module",
6
6
  "exports": "./lib/index.js",
@@ -71,12 +71,12 @@
71
71
  "@netlify/cache-utils": "^6.0.5",
72
72
  "@netlify/config": "^24.4.4",
73
73
  "@netlify/edge-bundler": "14.9.19",
74
- "@netlify/functions-utils": "^6.2.28",
74
+ "@netlify/functions-utils": "^6.2.29",
75
75
  "@netlify/git-utils": "^6.0.4",
76
76
  "@netlify/opentelemetry-utils": "^2.0.2",
77
77
  "@netlify/plugins-list": "^6.81.3",
78
78
  "@netlify/run-utils": "^6.0.3",
79
- "@netlify/zip-it-and-ship-it": "14.5.2",
79
+ "@netlify/zip-it-and-ship-it": "14.5.3",
80
80
  "@sindresorhus/slugify": "^2.0.0",
81
81
  "ansi-escapes": "^7.0.0",
82
82
  "ansis": "^4.1.0",
@@ -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": "3a8928c89ea24449e8462bcc7e7065a07fc691e4"
156
+ "gitHead": "85ec413dbce5342df01289953b91169c1f84c34b"
156
157
  }