Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 21 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,32 @@ jobs:
- name: Checkout
uses: actions/checkout@v2

- name: Tests folders that should work
- name: Test all rules
uses: ./
with:
path: './stubs'
path: './stubs/all_rules'

- name: Tests folders are ignored
- name: Test ignore option
uses: ./
with:
path: './stubs'
path: './stubs/all_rules'
ignore: |
20250101000000_should_work
20240101000000_should_also_work

- name: Test only name rules
uses: ./
with:
path: './stubs/name_rules'
rules: |
date
format

- name: Test only file rules
uses: ./
with:
path: './stubs/files_rules'
rules: |
missing
link
transaction
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2025 Zoey Kaiser
Copyright (c) 2025 SIDESTREAM GmbH

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
41 changes: 35 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,16 @@

**Lint Prisma migrations** is a github action that can lint the names of your [Prisma Migrations](https://www.prisma.io/docs/orm/prisma-migrate) based on a set of rules. The action was originally developed for [SIDESTREAM](https://github.com/sidestream-tech/).

The action checks that:
- The name matches the format of [`YYYYMMDDHHMMSS_YOUR_MIGRATION_NAME`](https://regex101.com/r/GoZmJG/1)
- The date specified inside the migration name is not in the future
## Rules

## Example
This action currently supports the following rules:
- `format`: The name of the migration folder matches [`YYYYMMDDHHMMSS_YOUR_MIGRATION_NAME`](https://regex101.com/r/GoZmJG/1)
- `date`: The date specified inside the migration name is not in the future
- `missing`: The migration folder contains a `migration.sql` file
- `link`: The migration begins with a link to the PR it was added it: `-- https://github.com/ORG/REPO/pull/NUMBER`
- `transaction`: The migration is wrapped with a `BEGIN;` and `COMMIT;` [transaction block](https://www.postgresql.org/docs/current/tutorial-transactions.html)

## Configuration

```yml
jobs:
Expand All @@ -20,13 +25,17 @@ jobs:
uses: sidestream-tech/lint-prisma-migrations@1.1.0
with:
path: ./prisma/migrations/
rules: |
format
date
missing
link
transaction
ignore: |
20260101000000_ignore_me
20270101000000_ignore_me_too
```

## Inputs

### `path`

The path to the Prisma migrations folder.
Expand All @@ -35,6 +44,26 @@ The path to the Prisma migrations folder.

A multiline input of migration names to ignore. Helpful if these were already applied and their naming cannot be fixed.

### `rules`

A multiline input of the rules you would like to run again your migrations. If not set all rules with be enabled by default.

## Development

```sh
# Install dependencies
pnpm install

# Run unit tests
pnpm test

# Build the project
pnpm build

# Package the build (run after `pnpm build`)
pnpm package
```

## Credits

This action was inspired by https://github.com/batista/lint-filenames
3 changes: 3 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ inputs:
ignore:
description: 'A list of migration names to ignore'
required: false
rules:
description: 'The list of rules you would like to run'
required: false
outputs:
total-files-analyzed:
description: 'The number of files analyzed.'
Expand Down
23 changes: 13 additions & 10 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,9 @@ function run() {
return __awaiter(this, void 0, void 0, function* () {
try {
const path = core.getInput('path', { required: false }) || DEFAULT_PATH;
const ignore = core.getMultilineInput('ignore', { required: false }) || [];
const output = yield (0, validate_1.validate)(path, ignore);
const ignore = core.getMultilineInput('ignore', { required: false });
const rules = core.getMultilineInput('rules', { required: false });
const output = yield (0, validate_1.validate)(path, { ignore, rules });
core.setOutput('total-files-analyzed', output.totalFilesAnalyzed);
// Get the JSON webhook payload for the event that triggered the workflow
const payload = JSON.stringify(github.context.payload, undefined, 2);
Expand Down Expand Up @@ -176,10 +177,12 @@ const date_1 = __nccwpck_require__(8752);
const format_1 = __nccwpck_require__(4719);
const link_1 = __nccwpck_require__(4656);
const transaction_1 = __nccwpck_require__(4354);
function validate(path, ignore) {
const DEFAULT_RULES = ['date', 'format', 'missing', 'link', 'transaction'];
function validate(path, options) {
return __awaiter(this, void 0, void 0, function* () {
var _a, e_1, _b, _c;
console.log(`Validating migrations at ${path}`);
const rules = options.rules.length > 0 ? options.rules : DEFAULT_RULES;
console.log(`Validating migrations at ${path} with rules: ${rules.join(', ')}`);
console.log('---------------------------------------------------------');
const opendir = node_fs_1.default.promises.opendir;
const existsSync = node_fs_1.default.existsSync;
Expand All @@ -198,39 +201,39 @@ function validate(path, ignore) {
continue;
}
// Check if migration is in ignore folder
if (ignore.includes(dirent.name)) {
if (options.ignore.includes(dirent.name)) {
console.log(`🟠 Migration ${dirent.name} is ignored`);
continue;
}
totalFilesAnalyzed++;
// Test 1: Does the name match the pattern?
if (!(0, format_1.isFormatValid)(dirent.name)) {
if (!(0, format_1.isFormatValid)(dirent.name) && rules.includes('format')) {
console.log(`❌ Migration ${dirent.name} is invalid format`);
failedFiles.push({ name: dirent.name, reason: 'format' });
continue;
}
// Test 2: Is the date in the folder name in the past?
if (!(0, date_1.isDateValid)(dirent.name)) {
if (!(0, date_1.isDateValid)(dirent.name) && rules.includes('date')) {
console.log(`❌ Migration ${dirent.name} is invalid date`);
failedFiles.push({ name: dirent.name, reason: 'date' });
continue;
}
// Test 3: Does the migration folder contain a migration.sql file?
const filePath = (0, ufo_1.joinURL)(dirent.parentPath, dirent.name, 'migration.sql');
if (!existsSync(filePath)) {
if (!existsSync(filePath) && rules.includes('missing')) {
console.log(`❌ Migration ${dirent.name} does not contain a migration.sql file`);
failedFiles.push({ name: dirent.name, reason: 'missing' });
continue;
}
const migration = yield readFile(filePath, 'utf8');
// Test 4: Does the migration file have a PR linked at the top?
if (!(0, link_1.hasPRLink)(migration)) {
if (!(0, link_1.hasPRLink)(migration) && rules.includes('link')) {
console.log(`❌ Migration ${dirent.name} does not have a PR linked at the top of the migration`);
failedFiles.push({ name: dirent.name, reason: 'link' });
continue;
}
// Test 5: Is the migration wrapped in a transaction block?
if (!(0, transaction_1.hasTransactionWrapper)(migration)) {
if (!(0, transaction_1.hasTransactionWrapper)(migration) && rules.includes('transaction')) {
console.log(`❌ Migration ${dirent.name} is not wrapped in a transaction block`);
failedFiles.push({ name: dirent.name, reason: 'transaction' });
continue;
Expand Down
2 changes: 1 addition & 1 deletion dist/index.js.map

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ const DEFAULT_PATH = './prisma/migrations/'
async function run(): Promise<void> {
try {
const path = core.getInput('path', { required: false }) || DEFAULT_PATH
const ignore = core.getMultilineInput('ignore', { required: false }) || []
const ignore = core.getMultilineInput('ignore', { required: false })
const rules = core.getMultilineInput('rules', { required: false })

const output = await validate(path, ignore)
const output = await validate(path, { ignore, rules })

core.setOutput('total-files-analyzed', output.totalFilesAnalyzed)

Expand Down
31 changes: 19 additions & 12 deletions src/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,25 @@ import { isFormatValid } from './rules/format'
import { hasPRLink } from './rules/link'
import { hasTransactionWrapper } from './rules/transaction'

export async function validate(path: string, ignore: string[]) {
console.log(`Validating migrations at ${path}`)
type Rule = 'format' | 'date' | 'missing' | 'link' | 'transaction'
const DEFAULT_RULES: Rule[] = ['date', 'format', 'missing', 'link', 'transaction']

interface ValidateOptions {
ignore: string[]
rules: string[]
}

export async function validate(path: string, options: ValidateOptions) {
const rules = options.rules.length > 0 ? options.rules : DEFAULT_RULES

console.log(`Validating migrations at ${path} with rules: ${rules.join(', ')}`)
console.log('---------------------------------------------------------')

const opendir = fs.promises.opendir
const existsSync = fs.existsSync
const readFile = fs.promises.readFile

const failedFiles: {
name: string
reason: 'format' | 'date' | 'missing' | 'link' | 'transaction'
}[] = []
const failedFiles: { name: string, reason: Rule }[] = []
let totalFilesAnalyzed = 0

try {
Expand All @@ -29,30 +36,30 @@ export async function validate(path: string, ignore: string[]) {
}

// Check if migration is in ignore folder
if (ignore.includes(dirent.name)) {
if (options.ignore.includes(dirent.name)) {
console.log(`🟠 Migration ${dirent.name} is ignored`)
continue
}

totalFilesAnalyzed++

// Test 1: Does the name match the pattern?
if (!isFormatValid(dirent.name)) {
if (!isFormatValid(dirent.name) && rules.includes('format')) {
console.log(`❌ Migration ${dirent.name} is invalid format`)
failedFiles.push({ name: dirent.name, reason: 'format' })
continue
}

// Test 2: Is the date in the folder name in the past?
if (!isDateValid(dirent.name)) {
if (!isDateValid(dirent.name) && rules.includes('date')) {
console.log(`❌ Migration ${dirent.name} is invalid date`)
failedFiles.push({ name: dirent.name, reason: 'date' })
continue
}

// Test 3: Does the migration folder contain a migration.sql file?
const filePath = joinURL(dirent.parentPath, dirent.name, 'migration.sql')
if (!existsSync(filePath)) {
if (!existsSync(filePath) && rules.includes('missing')) {
console.log(`❌ Migration ${dirent.name} does not contain a migration.sql file`)
failedFiles.push({ name: dirent.name, reason: 'missing' })
continue
Expand All @@ -61,14 +68,14 @@ export async function validate(path: string, ignore: string[]) {
const migration = await readFile(filePath, 'utf8')

// Test 4: Does the migration file have a PR linked at the top?
if (!hasPRLink(migration)) {
if (!hasPRLink(migration) && rules.includes('link')) {
console.log(`❌ Migration ${dirent.name} does not have a PR linked at the top of the migration`)
failedFiles.push({ name: dirent.name, reason: 'link' })
continue
}

// Test 5: Is the migration wrapped in a transaction block?
if (!hasTransactionWrapper(migration)) {
if (!hasTransactionWrapper(migration) && rules.includes('transaction')) {
console.log(`❌ Migration ${dirent.name} is not wrapped in a transaction block`)
failedFiles.push({ name: dirent.name, reason: 'transaction' })
continue
Expand Down
File renamed without changes.
15 changes: 15 additions & 0 deletions stubs/files_rules/20300101000000_invalid_date/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
-- https://github.com/sidestream-tech/lint-prisma-migrations/pull/1
BEGIN;

-- CreateEnum
CREATE TYPE "Enum" AS ENUM ('One', 'Two');

-- CreateTable
CREATE TABLE "MyTable" (
"id" TEXT NOT NULL,
"message" TEXT NOT NULL,

CONSTRAINT "TaskError_pkey" PRIMARY KEY ("id")
);

COMMIT;
15 changes: 15 additions & 0 deletions stubs/files_rules/invalid_format/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
-- https://github.com/sidestream-tech/lint-prisma-migrations/pull/1
BEGIN;

-- CreateEnum
CREATE TYPE "Enum" AS ENUM ('One', 'Two');

-- CreateTable
CREATE TABLE "MyTable" (
"id" TEXT NOT NULL,
"message" TEXT NOT NULL,

CONSTRAINT "TaskError_pkey" PRIMARY KEY ("id")
);

COMMIT;
Empty file.
12 changes: 12 additions & 0 deletions stubs/name_rules/20240101000000_no_transaction/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
-- https://github.com/sidestream-tech/lint-prisma-migrations/pull/1

-- CreateEnum
CREATE TYPE "Enum" AS ENUM ('One', 'Two');

-- CreateTable
CREATE TABLE "MyTable" (
"id" TEXT NOT NULL,
"message" TEXT NOT NULL,

CONSTRAINT "TaskError_pkey" PRIMARY KEY ("id")
);
14 changes: 14 additions & 0 deletions stubs/name_rules/20250101000000_no_pr_link/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
BEGIN;

-- CreateEnum
CREATE TYPE "Enum" AS ENUM ('One', 'Two');

-- CreateTable
CREATE TABLE "MyTable" (
"id" TEXT NOT NULL,
"message" TEXT NOT NULL,

CONSTRAINT "TaskError_pkey" PRIMARY KEY ("id")
);

COMMIT;
Empty file.