initial commit
All checks were successful
CI / build (push) Successful in 17s
CI / release (push) Has been skipped

This commit is contained in:
2026-03-05 21:57:44 +11:00
commit 1c2214b0f4
7 changed files with 516 additions and 0 deletions

77
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,77 @@
name: CI
on:
push:
branches: ["main"]
tags: ["v*"]
pull_request:
branches: ["main"]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: 20
cache: npm
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
- name: Check dist is up to date
if: github.ref_type != 'tag'
run: |
if [ -n "$(git status --short dist/)" ]; then
echo "dist/ is out of date. Run 'npm run build' and commit the result."
git diff dist/
exit 1
fi
release:
runs-on: ubuntu-latest
needs: build
if: github.ref_type == 'tag'
permissions:
contents: write
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: 20
cache: npm
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
- name: Make sure tag matches package.json version
run: |
TAG_VERSION="${GITHUB_REF#refs/tags/}"
PACKAGE_VERSION=$(node -p "require('./package.json').version")
if [ "$TAG_VERSION" != "$PACKAGE_VERSION" ]; then
echo "Tag version ($TAG_VERSION) does not match package.json version ($PACKAGE_VERSION)"
exit 1
fi
- name: Commit dist to tag
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add --force dist/
git commit -m "chore: build dist for ${{ github.ref_name }}" || echo "Nothing to commit"
git tag -f ${{ github.ref_name }}
git push origin ${{ github.ref_name }} --force
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
generate_release_notes: true

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
node_modules/
dist/

45
README.md Normal file
View File

@@ -0,0 +1,45 @@
# Pending Payload Migration Check Action
This action will check your root project (or any specified projects) for pending PayloadCMS migrations and if it finds any it will report an error to stop deployments. This is useful to prevent deploying code that requires migrations without running them first.
NOTE: This repo is hosted at https://liamsgit.dev/LiamPietralla/pending-payload-migration, the GitHub repo is just a push mirror.
## Usage
```yaml
name: Check for Pending Payload Migrations
on:
push:
branches:
- main
jobs:
check-migrations:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v3
with:
node-version: '18'
- run: npm install # Install dependencies before running the action
- name: Check for Pending Payload Migrations
uses: LiamPietralla/pending-payload-migration-action@v0.1.0
with:
paths: "path/to/your/payload/project" # Optional, defaults to root of the repository
```
## Inputs
- `paths`: The paths to check for pending migrations, allows multiple as array. Defaults to the root of the repository.
## Outputs
- `has-pending-migrations`: Whether there are pending migrations or not. This can be used in subsequent steps to conditionally run migration commands or fail the workflow.
## Release Steps
To release a new version of the action, follow these steps:
1. Update the version number in `package.json` and commit.
2. Push the commit to `main`.
3. Tag the commit and push the tag (e.g., `git tag v1.0.0 && git push origin v1.0.0`).
The CI pipeline will automatically build `dist/`, commit it to the tag, and create a GitHub Release.

15
action.yml Normal file
View File

@@ -0,0 +1,15 @@
name: Pending Payload Migration Check
description: Check whether pending migrations are pending for PayloadCMS and if so report errors to stop deployments.
inputs:
paths:
description: The paths to check for pending migrations, allows multiple as array. Defaults to the root of the repository.
required: false
default: "."
outputs:
has-pending-migrations:
description: Whether there are pending migrations or not
runs:
using: node20
main: dist/index.js

270
package-lock.json generated Normal file
View File

@@ -0,0 +1,270 @@
{
"name": "pending-payload-migration-action",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "pending-payload-migration-action",
"version": "0.1.0",
"license": "ISC",
"dependencies": {
"@actions/core": "^3.0.0",
"@actions/github": "^9.0.0"
},
"devDependencies": {
"@vercel/ncc": "^0.38.4"
}
},
"node_modules/@actions/core": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@actions/core/-/core-3.0.0.tgz",
"integrity": "sha512-zYt6cz+ivnTmiT/ksRVriMBOiuoUpDCJJlZ5KPl2/FRdvwU3f7MPh9qftvbkXJThragzUZieit2nyHUyw53Seg==",
"license": "MIT",
"dependencies": {
"@actions/exec": "^3.0.0",
"@actions/http-client": "^4.0.0"
}
},
"node_modules/@actions/exec": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@actions/exec/-/exec-3.0.0.tgz",
"integrity": "sha512-6xH/puSoNBXb72VPlZVm7vQ+svQpFyA96qdDBvhB8eNZOE8LtPf9L4oAsfzK/crCL8YZ+19fKYVnM63Sl+Xzlw==",
"license": "MIT",
"dependencies": {
"@actions/io": "^3.0.2"
}
},
"node_modules/@actions/github": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/@actions/github/-/github-9.0.0.tgz",
"integrity": "sha512-yJ0RoswsAaKcvkmpCE4XxBRiy/whH2SdTBHWzs0gi4wkqTDhXMChjSdqBz/F4AeiDlP28rQqL33iHb+kjAMX6w==",
"license": "MIT",
"dependencies": {
"@actions/http-client": "^3.0.2",
"@octokit/core": "^7.0.6",
"@octokit/plugin-paginate-rest": "^14.0.0",
"@octokit/plugin-rest-endpoint-methods": "^17.0.0",
"@octokit/request": "^10.0.7",
"@octokit/request-error": "^7.1.0",
"undici": "^6.23.0"
}
},
"node_modules/@actions/github/node_modules/@actions/http-client": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-3.0.2.tgz",
"integrity": "sha512-JP38FYYpyqvUsz+Igqlc/JG6YO9PaKuvqjM3iGvaLqFnJ7TFmcLyy2IDrY0bI0qCQug8E9K+elv5ZNfw62ZJzA==",
"license": "MIT",
"dependencies": {
"tunnel": "^0.0.6",
"undici": "^6.23.0"
}
},
"node_modules/@actions/http-client": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-4.0.0.tgz",
"integrity": "sha512-QuwPsgVMsD6qaPD57GLZi9sqzAZCtiJT8kVBCDpLtxhL5MydQ4gS+DrejtZZPdIYyB1e95uCK9Luyds7ybHI3g==",
"license": "MIT",
"dependencies": {
"tunnel": "^0.0.6",
"undici": "^6.23.0"
}
},
"node_modules/@actions/io": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@actions/io/-/io-3.0.2.tgz",
"integrity": "sha512-nRBchcMM+QK1pdjO7/idu86rbJI5YHUKCvKs0KxnSYbVe3F51UfGxuZX4Qy/fWlp6l7gWFwIkrOzN+oUK03kfw==",
"license": "MIT"
},
"node_modules/@octokit/auth-token": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-6.0.0.tgz",
"integrity": "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==",
"license": "MIT",
"engines": {
"node": ">= 20"
}
},
"node_modules/@octokit/core": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/@octokit/core/-/core-7.0.6.tgz",
"integrity": "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q==",
"license": "MIT",
"dependencies": {
"@octokit/auth-token": "^6.0.0",
"@octokit/graphql": "^9.0.3",
"@octokit/request": "^10.0.6",
"@octokit/request-error": "^7.0.2",
"@octokit/types": "^16.0.0",
"before-after-hook": "^4.0.0",
"universal-user-agent": "^7.0.0"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/@octokit/endpoint": {
"version": "11.0.3",
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.3.tgz",
"integrity": "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag==",
"license": "MIT",
"dependencies": {
"@octokit/types": "^16.0.0",
"universal-user-agent": "^7.0.2"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/@octokit/graphql": {
"version": "9.0.3",
"resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-9.0.3.tgz",
"integrity": "sha512-grAEuupr/C1rALFnXTv6ZQhFuL1D8G5y8CN04RgrO4FIPMrtm+mcZzFG7dcBm+nq+1ppNixu+Jd78aeJOYxlGA==",
"license": "MIT",
"dependencies": {
"@octokit/request": "^10.0.6",
"@octokit/types": "^16.0.0",
"universal-user-agent": "^7.0.0"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/@octokit/openapi-types": {
"version": "27.0.0",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-27.0.0.tgz",
"integrity": "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA==",
"license": "MIT"
},
"node_modules/@octokit/plugin-paginate-rest": {
"version": "14.0.0",
"resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-14.0.0.tgz",
"integrity": "sha512-fNVRE7ufJiAA3XUrha2omTA39M6IXIc6GIZLvlbsm8QOQCYvpq/LkMNGyFlB1d8hTDzsAXa3OKtybdMAYsV/fw==",
"license": "MIT",
"dependencies": {
"@octokit/types": "^16.0.0"
},
"engines": {
"node": ">= 20"
},
"peerDependencies": {
"@octokit/core": ">=6"
}
},
"node_modules/@octokit/plugin-rest-endpoint-methods": {
"version": "17.0.0",
"resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-17.0.0.tgz",
"integrity": "sha512-B5yCyIlOJFPqUUeiD0cnBJwWJO8lkJs5d8+ze9QDP6SvfiXSz1BF+91+0MeI1d2yxgOhU/O+CvtiZ9jSkHhFAw==",
"license": "MIT",
"dependencies": {
"@octokit/types": "^16.0.0"
},
"engines": {
"node": ">= 20"
},
"peerDependencies": {
"@octokit/core": ">=6"
}
},
"node_modules/@octokit/request": {
"version": "10.0.8",
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.8.tgz",
"integrity": "sha512-SJZNwY9pur9Agf7l87ywFi14W+Hd9Jg6Ifivsd33+/bGUQIjNujdFiXII2/qSlN2ybqUHfp5xpekMEjIBTjlSw==",
"license": "MIT",
"dependencies": {
"@octokit/endpoint": "^11.0.3",
"@octokit/request-error": "^7.0.2",
"@octokit/types": "^16.0.0",
"fast-content-type-parse": "^3.0.0",
"json-with-bigint": "^3.5.3",
"universal-user-agent": "^7.0.2"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/@octokit/request-error": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.1.0.tgz",
"integrity": "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw==",
"license": "MIT",
"dependencies": {
"@octokit/types": "^16.0.0"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/@octokit/types": {
"version": "16.0.0",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-16.0.0.tgz",
"integrity": "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg==",
"license": "MIT",
"dependencies": {
"@octokit/openapi-types": "^27.0.0"
}
},
"node_modules/@vercel/ncc": {
"version": "0.38.4",
"resolved": "https://registry.npmjs.org/@vercel/ncc/-/ncc-0.38.4.tgz",
"integrity": "sha512-8LwjnlP39s08C08J5NstzriPvW1SP8Zfpp1BvC2sI35kPeZnHfxVkCwu4/+Wodgnd60UtT1n8K8zw+Mp7J9JmQ==",
"dev": true,
"license": "MIT",
"bin": {
"ncc": "dist/ncc/cli.js"
}
},
"node_modules/before-after-hook": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-4.0.0.tgz",
"integrity": "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==",
"license": "Apache-2.0"
},
"node_modules/fast-content-type-parse": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-3.0.0.tgz",
"integrity": "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"license": "MIT"
},
"node_modules/json-with-bigint": {
"version": "3.5.7",
"resolved": "https://registry.npmjs.org/json-with-bigint/-/json-with-bigint-3.5.7.tgz",
"integrity": "sha512-7ei3MdAI5+fJPVnKlW77TKNKwQ5ppSzWvhPuSuINT/GYW9ZOC1eRKOuhV9yHG5aEsUPj9BBx5JIekkmoLHxZOw==",
"license": "MIT"
},
"node_modules/tunnel": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz",
"integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==",
"license": "MIT",
"engines": {
"node": ">=0.6.11 <=0.7.0 || >=0.7.3"
}
},
"node_modules/undici": {
"version": "6.23.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.23.0.tgz",
"integrity": "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==",
"license": "MIT",
"engines": {
"node": ">=18.17"
}
},
"node_modules/universal-user-agent": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz",
"integrity": "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==",
"license": "ISC"
}
}
}

31
package.json Normal file
View File

@@ -0,0 +1,31 @@
{
"name": "pending-payload-migration",
"version": "0.1.0",
"main": "index.js",
"scripts": {
"build": "ncc build src/index.js -o dist",
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "ssh://git@liamsgit.dev:2222/LiamPietralla/pending-payload-migration-action.git"
},
"keywords": [
"payload",
"migrations",
"github",
"actions",
"ci",
"cd"
],
"author": "Liam Pietralla",
"license": "ISC",
"description": "Check whether pending migrations are pending for PayloadCMS and if so report errors to stop deployments.",
"dependencies": {
"@actions/core": "^3.0.0",
"@actions/github": "^9.0.0"
},
"devDependencies": {
"@vercel/ncc": "^0.38.4"
}
}

76
src/index.js Normal file
View File

@@ -0,0 +1,76 @@
import * as core from "@actions/core";
import * as path from "node:path";
import * as fs from "node:fs";
import { execSync } from "node:child_process";
let hasPendingMigrations = false;
const MAX_DEPTH = 6;
// Recursively collect all .ts files inside any 'migrations' directory within a root
const collectMigrationFiles = (dir, depth = 0) => {
// Prevent infinite recursion by limiting depth
if (depth > MAX_DEPTH) return [];
// Store results
const results = [];
// Read directory entries, if it fails (e.g. due to permissions), return empty results
let entries;
try {
entries = fs.readdirSync(dir, { withFileTypes: true });
} catch {
return results;
}
// Process each entry in the directory
for (const entry of entries) {
// Skip node_modules to avoid unnecessary processing
if (entry.name === "node_modules") continue;
const fullPath = path.join(dir, entry.name);
// Find the migrations directory and collect .ts files, otherwise continue searching subdirectories
if (entry.isDirectory()) {
if (entry.name === "migrations") {
for (const f of fs.readdirSync(fullPath)) {
if (f.endsWith(".ts")) results.push(path.join(fullPath, f));
}
} else {
results.push(...collectMigrationFiles(fullPath, depth + 1));
}
}
}
return results;
}
try {
// Get the input 'paths'
const paths = core.getInput("paths").split("\n").map(path => path.trim()).filter(path => path.length > 0);
// Log the paths to check for pending migrations
for (const projectPath of paths) {
const projectDir = projectPath;
core.info(`Checking for pending migrations in path: ${projectPath}`);
// Snapshot all existing migration .ts files before running the command
const beforeFiles = new Set(collectMigrationFiles(projectDir));
// Run the migrate check command, a dummy secret is required as otherwise the command will error before it can check for pending migrations
execSync("npm run payload migrate:create --skip-empty", { cwd: projectDir, stdio: "ignore", env: { ...process.env, PAYLOAD_SECRET: "PAYLOAD_SECRET" } });
// Find any new migration .ts files by comparing snapshots
const newMigrations = collectMigrationFiles(projectDir).filter(f => !beforeFiles.has(f));
// If there are new migration files, we have pending migrations
if (newMigrations.length > 0) {
hasPendingMigrations = true;
core.setFailed(`Pending migrations detected in path: ${projectPath}.`);
break;
}
}
core.setOutput("has-pending-migrations", hasPendingMigrations);
} catch (error) {
core.setFailed(error instanceof Error ? error.message : String(error));
}