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
9 changes: 6 additions & 3 deletions .ai/skills/release/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,12 @@ description: Checklist for releasing packages from this monorepo (code PR -> Ver
2. Run `corepack yarn verify:full` locally once before pushing.
- This is the fastest way to catch the common CI-only failure: transloadit parity drift in `Verify (full)`.
3. If `verify:full` (or CI `Verify (full)`) fails with transloadit parity drift, apply the “Parity drift playbook” below, then re-run `corepack yarn verify:full`.
4. Commit + push branch
5. Open PR, wait for CI green
6. Squash-merge the PR
4. If you add a changeset for `@transloadit/node`, also add a similar changeset for `@transloadit/mcp-server` if it could affect its workings. The chances are, they are, since the latter is mostly a thin wrapper around the former.
- This repo enforces a one-way coupling: node releases should also publish a new mcp-server version (but mcp-server releases do not require node releases).
- `yarn check`/`yarn verify` will fail fast if you forget.
5. Commit + push branch
6. Open PR, wait for CI green
7. Squash-merge the PR

Notes:
1. When creating PRs with `gh pr create` from a shell, avoid unescaped backticks in the `--body` string.
Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@
"packages/*"
],
"scripts": {
"check": "yarn fix:deps && yarn fix:js && yarn lint:ts && yarn test:unit",
"verify": "yarn lint:publish && yarn lint:deps && yarn lint:js && yarn lint:ts && yarn test:unit",
"check": "yarn lint:changesets && yarn fix:deps && yarn fix:js && yarn lint:ts && yarn test:unit",
"verify": "yarn lint:changesets && yarn lint:publish && yarn lint:deps && yarn lint:js && yarn lint:ts && yarn test:unit",
"verify:full": "yarn verify && yarn knip && yarn parity:transloadit && yarn test:types",
"lint:js": "biome check .",
"lint:ts": "yarn tsc:types && yarn tsc:node && yarn tsc:zod",
"lint:changesets": "node scripts/guard-changesets.ts",
"lint": "yarn lint:js",
"fix": "yarn fix:js",
"fix:js": "biome check --write .",
Expand Down
100 changes: 100 additions & 0 deletions scripts/guard-changesets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import fs from 'node:fs/promises'
import path from 'node:path'

type BumpType = 'major' | 'minor' | 'patch'

type ParsedChangeset = {
file: string
packages: Set<string>
}

const CHANGESET_DIR = path.join(process.cwd(), '.changeset')
const NODE_PKG = '@transloadit/node'
const MCP_SERVER_PKG = '@transloadit/mcp-server'

async function listChangesetFiles(): Promise<string[]> {
let entries: string[]
try {
entries = await fs.readdir(CHANGESET_DIR)
} catch {
return []
}

return entries
.filter((name) => name.endsWith('.md'))
.filter((name) => name.toLowerCase() !== 'readme.md')
.map((name) => path.join(CHANGESET_DIR, name))
}

function parseFrontmatterPackages(markdown: string): Set<string> {
// Changesets frontmatter is YAML-ish and looks like:
// ---
// "@transloadit/node": patch
// "@transloadit/mcp-server": patch
// ---
const first = markdown.indexOf('---')
if (first === -1) return new Set()
const second = markdown.indexOf('---', first + 3)
if (second === -1) return new Set()

const frontmatter = markdown.slice(first + 3, second)
const pkgs = new Set<string>()
const re = /["']([^"']+)["']\s*:\s*(major|minor|patch)\b/g

for (const match of frontmatter.matchAll(re)) {
const pkg = match[1]
const bump = match[2] as BumpType
if (pkg && bump) pkgs.add(pkg)
}
return pkgs
}

async function parseChangesets(files: string[]): Promise<ParsedChangeset[]> {
const out: ParsedChangeset[] = []
for (const file of files) {
const markdown = await fs.readFile(file, 'utf8')
out.push({ file, packages: parseFrontmatterPackages(markdown) })
}
return out
}

function fail(message: string): never {
// stderr only, so it plays well with JSON-only tools.
process.stderr.write(`${message}\n`)
process.exit(1)
}

async function main(): Promise<void> {
const files = await listChangesetFiles()
if (files.length === 0) return

const changesets = await parseChangesets(files)
const touched = new Set<string>()
for (const cs of changesets) {
for (const pkg of cs.packages) touched.add(pkg)
}

// One-way coupling policy:
// If @transloadit/node is being released, also release @transloadit/mcp-server
// so the published mcp-server versions stay "in sync" with node evolution.
const touchesNode = touched.has(NODE_PKG)
const touchesMcpServer = touched.has(MCP_SERVER_PKG)

if (touchesNode && !touchesMcpServer) {
fail(
[
`Changeset policy violation: ${NODE_PKG} is being released, but ${MCP_SERVER_PKG} is not.`,
'',
`Add a patch changeset for ${MCP_SERVER_PKG} (even if no code changed) so the published MCP server`,
'can be tracked as validated against the latest @transloadit/node.',
'',
'Example:',
' corepack yarn changeset',
` (select ${MCP_SERVER_PKG} -> patch)`,
` Summary: "chore: release mcp-server alongside @transloadit/node"`,
].join('\n'),
)
}
}

await main()