Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz Source: /opt/alga-psa on psa.joliet.tech
159 lines
5.0 KiB
JavaScript
159 lines
5.0 KiB
JavaScript
import path from 'path';
|
|
|
|
/**
|
|
* ESLint rule to enforce proper migration file naming conventions.
|
|
*
|
|
* Migration files must:
|
|
* 1. Be named with yyyymmddhhmm or yyyymmddhhmmss prefix (e.g., 202410311430_description.cjs or 20241031143045_description.cjs)
|
|
* 2. Have timestamps that are not in the future (compared to current date)
|
|
*
|
|
* This rule only applies to files in directories containing "migration" in the path.
|
|
*
|
|
* IMPORTANT: Migrations before 2025-11-18 are grandfathered in and will not be checked.
|
|
* This prevents errors on existing legacy migrations that don't follow the naming convention.
|
|
*/
|
|
|
|
const MIGRATION_FILENAME_PATTERN = /^(\d{12}(?:\d{2})?)_.*\.cjs$/;
|
|
// Only enforce the rule for migrations on or after this date (yyyymmdd format)
|
|
const CUTOFF_DATE = '20251118';
|
|
|
|
function parseTimestamp(timestampStr) {
|
|
// Parse yyyymmddhhmm or yyyymmddhhmmss format
|
|
const year = parseInt(timestampStr.substring(0, 4), 10);
|
|
const month = parseInt(timestampStr.substring(4, 6), 10);
|
|
const day = parseInt(timestampStr.substring(6, 8), 10);
|
|
const hour = parseInt(timestampStr.substring(8, 10), 10);
|
|
const minute = parseInt(timestampStr.substring(10, 12), 10);
|
|
const second = timestampStr.length >= 14 ? parseInt(timestampStr.substring(12, 14), 10) : 0;
|
|
|
|
// Validate ranges before creating Date object
|
|
// This prevents JavaScript from auto-correcting invalid dates (e.g., month 13 -> Jan next year)
|
|
if (month < 1 || month > 12) {
|
|
return { valid: false, date: null };
|
|
}
|
|
if (day < 1 || day > 31) {
|
|
return { valid: false, date: null };
|
|
}
|
|
if (hour < 0 || hour > 23) {
|
|
return { valid: false, date: null };
|
|
}
|
|
if (minute < 0 || minute > 59) {
|
|
return { valid: false, date: null };
|
|
}
|
|
if (second < 0 || second > 59) {
|
|
return { valid: false, date: null };
|
|
}
|
|
|
|
const date = new Date(year, month - 1, day, hour, minute, second); // JS months are 0-indexed
|
|
|
|
// Verify the date components weren't rolled over by JavaScript
|
|
// E.g., Feb 31 becomes Mar 3, so we need to check that didn't happen
|
|
if (
|
|
date.getFullYear() !== year ||
|
|
date.getMonth() !== month - 1 ||
|
|
date.getDate() !== day ||
|
|
date.getHours() !== hour ||
|
|
date.getMinutes() !== minute ||
|
|
date.getSeconds() !== second
|
|
) {
|
|
return { valid: false, date: null };
|
|
}
|
|
|
|
return { valid: true, date };
|
|
}
|
|
|
|
function isFutureDate(date) {
|
|
const now = new Date();
|
|
return date > now;
|
|
}
|
|
|
|
export default {
|
|
meta: {
|
|
type: "problem",
|
|
docs: {
|
|
description:
|
|
"Enforce proper migration file naming with yyyymmddhhmm prefix and prevent future timestamps",
|
|
recommended: true,
|
|
},
|
|
schema: [], // no options
|
|
messages: {
|
|
invalidFormat:
|
|
"Migration file '{{filename}}' must be named with yyyymmddhhmm or yyyymmddhhmmss prefix followed by underscore and description (e.g., 202410311430_add_users_table.cjs or 20241031143045_add_users_table.cjs)",
|
|
futureTimestamp:
|
|
"Migration file '{{filename}}' has a timestamp in the future ({{timestamp}}). Migration timestamps must not be later than the current date.",
|
|
invalidTimestamp:
|
|
"Migration file '{{filename}}' has an invalid timestamp '{{timestamp}}' that cannot be parsed as a valid date.",
|
|
},
|
|
},
|
|
|
|
create(context) {
|
|
const filename = context.getFilename();
|
|
const basename = path.basename(filename);
|
|
const dirname = path.dirname(filename);
|
|
|
|
// Only check files in directories that contain "migration" in the path
|
|
if (!dirname.includes('migration')) {
|
|
return {};
|
|
}
|
|
|
|
// Only check .cjs files (migration files are CommonJS)
|
|
if (!basename.endsWith('.cjs')) {
|
|
return {};
|
|
}
|
|
|
|
// Run the check once per file (on Program node)
|
|
return {
|
|
Program(node) {
|
|
// Extract the date prefix (first 8 digits = yyyymmdd) to check against cutoff
|
|
const datePrefix = basename.substring(0, 8);
|
|
|
|
// Skip validation for migrations before the cutoff date (grandfathered in)
|
|
if (datePrefix < CUTOFF_DATE) {
|
|
return;
|
|
}
|
|
|
|
const match = basename.match(MIGRATION_FILENAME_PATTERN);
|
|
|
|
if (!match) {
|
|
context.report({
|
|
node,
|
|
messageId: "invalidFormat",
|
|
data: {
|
|
filename: basename,
|
|
},
|
|
});
|
|
return;
|
|
}
|
|
|
|
const timestampStr = match[1];
|
|
const result = parseTimestamp(timestampStr);
|
|
|
|
if (!result.valid) {
|
|
context.report({
|
|
node,
|
|
messageId: "invalidTimestamp",
|
|
data: {
|
|
filename: basename,
|
|
timestamp: timestampStr,
|
|
},
|
|
});
|
|
return;
|
|
}
|
|
|
|
const timestamp = result.date;
|
|
if (isFutureDate(timestamp)) {
|
|
const formattedDate = timestamp.toISOString().replace('T', ' ').substring(0, 16);
|
|
context.report({
|
|
node,
|
|
messageId: "futureTimestamp",
|
|
data: {
|
|
filename: basename,
|
|
timestamp: formattedDate,
|
|
},
|
|
});
|
|
}
|
|
},
|
|
};
|
|
},
|
|
};
|