Skip to content
Draft
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
3 changes: 3 additions & 0 deletions .craft.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ targets:
- name: npm
id: "@sentry/junior"
includeNames: /^sentry-junior-\d.*\.tgz$/
- name: npm
id: "@sentry/junior-migrations"
includeNames: /^sentry-junior-migrations-\d.*\.tgz$/
- name: npm
id: "@sentry/junior-plugin-api"
includeNames: /^sentry-junior-plugin-api-\d.*\.tgz$/
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ jobs:
- .github/actions/**
core:
- packages/junior/**
- packages/junior-migrations/**
- packages/junior-plugin-api/**
- packages/junior-scheduler/**
- packages/junior-testing/**
Expand Down Expand Up @@ -215,6 +216,7 @@ jobs:
- uses: ./.github/actions/setup-node-pnpm
- run: pnpm --filter @sentry/junior-memory build
- run: pnpm --filter @sentry/junior-github build
- run: pnpm --filter @sentry/junior-migrations test
- run: pnpm --filter @sentry/junior-memory test
- run: pnpm --filter @sentry/junior-github test

Expand Down Expand Up @@ -341,6 +343,7 @@ jobs:
run: |
mkdir -p artifacts
pnpm --filter @sentry/junior pack --pack-destination artifacts
pnpm --filter @sentry/junior-migrations pack --pack-destination artifacts
pnpm --filter @sentry/junior-plugin-api pack --pack-destination artifacts
pnpm --filter @sentry/junior-agent-browser pack --pack-destination artifacts
pnpm --filter @sentry/junior-amplitude pack --pack-destination artifacts
Expand Down
1 change: 1 addition & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ pnpm build:pkg
This repo uses Craft for manual lockstep npm releases of:

- `@sentry/junior`
- `@sentry/junior-migrations`
- `@sentry/junior-plugin-api`
- `@sentry/junior-agent-browser`
- `@sentry/junior-amplitude`
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Start here:
| Package | Purpose |
| ------------------------------ | ---------------------------------------------------------------------------- |
| `@sentry/junior` | Core Slack bot runtime |
| `@sentry/junior-migrations` | Mixed Drizzle SQL and TypeScript migration format and runner |
| `@sentry/junior-plugin-api` | Lightweight plugin API types and helpers |
| `@sentry/junior-agent-browser` | Agent Browser plugin package for browser automation |
| `@sentry/junior-amplitude` | Read-only Amplitude product analytics through Amplitude's hosted MCP server |
Expand Down
2 changes: 1 addition & 1 deletion ast-grep/rules/no-core-plugin-dynamic-imports.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@ constraints:
all:
- regex: '^["@'']@sentry/junior-.+["@'']$'
- not:
regex: '^["@'']@sentry/junior-plugin-api["@'']$'
regex: '^["@'']@sentry/junior-(?:plugin-api|migrations)["@'']$'
2 changes: 1 addition & 1 deletion ast-grep/rules/no-core-plugin-reexports.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,4 @@ rule:
- not:
has:
kind: string
regex: '^["@'']@sentry/junior-plugin-api["@'']$'
regex: '^["@'']@sentry/junior-(?:plugin-api|migrations)["@'']$'
2 changes: 1 addition & 1 deletion ast-grep/rules/no-core-plugin-static-imports.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,4 @@ rule:
- not:
has:
kind: string
regex: '^["@'']@sentry/junior-plugin-api["@'']$'
regex: '^["@'']@sentry/junior-(?:plugin-api|migrations)["@'']$'
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,19 @@
"worktree:setup": "node scripts/worktree.mjs setup",
"cloudflare:token": "node scripts/refresh-cloudflare-tunnel-token.mjs",
"prepare": "simple-git-hooks",
"lint": "pnpm --filter @sentry/junior lint && pnpm --filter @sentry/junior-memory lint && pnpm --filter @sentry/junior-github lint && pnpm ast-grep:lint && pnpm package:lint",
"lint": "pnpm --filter @sentry/junior-migrations lint && pnpm --filter @sentry/junior lint && pnpm --filter @sentry/junior-memory lint && pnpm --filter @sentry/junior-github lint && pnpm ast-grep:lint && pnpm package:lint",
"lint:fix": "pnpm --filter @sentry/junior lint:fix",
"ast-grep:lint": "ast-grep scan",
"lint-staged": "lint-staged",
"build": "pnpm --filter @sentry/junior build && pnpm --filter @sentry/junior-memory build && pnpm --filter @sentry/junior-github build && pnpm --filter @sentry/junior-dashboard build",
"build": "pnpm --filter @sentry/junior-migrations build && pnpm --filter @sentry/junior build && pnpm --filter @sentry/junior-memory build && pnpm --filter @sentry/junior-github build && pnpm --filter @sentry/junior-dashboard build",
"build:example": "pnpm --filter @sentry/junior-example build",
"docs:dev": "pnpm --filter @sentry/junior-docs dev",
"docs:build": "pnpm --filter @sentry/junior-docs build",
"docs:check": "pnpm --filter @sentry/junior-docs check",
"package:lint": "for pkg in packages/junior packages/junior-plugin-api packages/junior-scheduler packages/junior-memory packages/junior-dashboard packages/junior-github packages/junior-agent-browser packages/junior-amplitude packages/junior-cloudflare packages/junior-datadog packages/junior-hex packages/junior-linear packages/junior-maintenance packages/junior-notion packages/junior-sentry packages/junior-vercel; do pnpm exec publint \"$pkg\" || exit $?; done",
"package:lint": "for pkg in packages/junior packages/junior-migrations packages/junior-plugin-api packages/junior-scheduler packages/junior-memory packages/junior-dashboard packages/junior-github packages/junior-agent-browser packages/junior-amplitude packages/junior-cloudflare packages/junior-datadog packages/junior-hex packages/junior-linear packages/junior-maintenance packages/junior-notion packages/junior-sentry packages/junior-vercel; do pnpm exec publint \"$pkg\" || exit $?; done",
"release:check": "node scripts/check-release-config.mjs",
"start": "pnpm --filter @sentry/junior-example dev",
"test": "pnpm --filter @sentry/junior build && pnpm --filter @sentry/junior-memory build && pnpm --filter @sentry/junior-github build && pnpm --filter @sentry/junior-dashboard build && pnpm --filter @sentry/junior test && pnpm --filter @sentry/junior-memory test && pnpm --filter @sentry/junior-github test && pnpm --filter @sentry/junior-dashboard test",
"test": "pnpm --filter @sentry/junior-migrations test && pnpm --filter @sentry/junior build && pnpm --filter @sentry/junior-memory build && pnpm --filter @sentry/junior-github build && pnpm --filter @sentry/junior-dashboard build && pnpm --filter @sentry/junior test && pnpm --filter @sentry/junior-memory test && pnpm --filter @sentry/junior-github test && pnpm --filter @sentry/junior-dashboard test",
"test:e2e:dashboard": "pnpm --filter @sentry/junior-dashboard build && playwright test -c packages/junior-dashboard/playwright.config.ts",
"test:watch": "pnpm --filter @sentry/junior test:watch",
"evals": "pnpm --filter @sentry/junior-evals evals",
Expand Down
34 changes: 25 additions & 9 deletions packages/docs/src/content/docs/cli/upgrade.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
---
title: "junior upgrade"
description: "Run one-shot Junior state upgrade migrations."
description: "Apply Junior schema and data migrations."
type: reference
summary: Move persisted Junior state forward after upgrading packages.
summary: Move configured SQL schemas and persisted state forward after upgrades.
prerequisites:
- /start-here/quickstart/
related:
Expand All @@ -11,7 +11,10 @@ related:
- /cli/snapshot-create/
---

Use `junior upgrade` after installing a Junior release that includes a one-shot state migration. The command mutates the configured state stores, so run it from the same app environment that has the production state and SQL environment variables configured for the deployment you are upgrading.
Use `junior upgrade` after installing a Junior release that includes schema or
data migrations. The command mutates the configured SQL database and state
stores, so run it from the same app environment that has the production state
and SQL environment variables configured for the deployment you are upgrading.

## Usage

Expand All @@ -25,12 +28,25 @@ The command takes no extra arguments.

## What it does

`junior upgrade` runs registered migrations sequentially. Current migrations:
`junior upgrade` runs migrations sequentially. Core and plugin migration
directories use Drizzle Kit's ordered journal and may contain either generated
SQL schema migrations or TypeScript data migrations targeting a versioned
host capability API. Current
upgrade work includes:

- Apply core and enabled-plugin SQL schema migrations.
- Rewrite retained turn-session records from legacy storage shapes before the
new runtime reads them.
- Move legacy `junior:conversation-work:*` Redis state into the newer conversation record and index state used by the durable worker and dashboard feed.
- Backfill retained conversation records into the shared Junior SQL database. The upgrade requires `DATABASE_URL`.

The migrations are idempotent: rerunning them skips records that were already moved, removes stale legacy index entries that no longer have a record, and upserts SQL conversation rows. The SQL conversation backfill copies a bounded legacy slice of Redis conversation metadata; after cutover, durable conversation metadata is written to SQL while Redis remains the transcript and execution/cache store.
Completed journal entries are tracked individually, and TypeScript migrations
can checkpoint progress for safe retries. Legacy backfills remain idempotent:
rerunning them skips records that were already moved, removes stale legacy
index entries that no longer have a record, and upserts SQL conversation rows.
The SQL conversation backfill copies a bounded legacy slice of Redis
conversation metadata; after cutover, durable conversation metadata is written
to SQL while Redis remains the transcript and execution/cache store.

## Vercel deploys

Expand All @@ -56,10 +72,10 @@ Typical logs look like this:

```text
Running Junior upgrade migrations...
Running migration migrate-redis-conversation-state...
Finished migration migrate-redis-conversation-state: scanned=2 migrated=1 existing=0 missing=1
Running migration backfill-conversations-sql...
Finished migration backfill-conversations-sql: scanned=2 migrated=2 existing=0 missing=0
Running migration core-migrations...
Finished migration core-migrations: scanned=8 migrated=4 existing=4 missing=0 skipped=0
Running migration plugin-migrations...
Finished migration plugin-migrations: scanned=8 migrated=8 existing=0 missing=0 skipped=0
Junior upgrade complete.
```

Expand Down
1 change: 1 addition & 0 deletions packages/docs/src/content/docs/contribute/releasing.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ related:
Junior uses lockstep package releases for:

- `@sentry/junior`
- `@sentry/junior-migrations`
- `@sentry/junior-plugin-api`
- `@sentry/junior-agent-browser`
- `@sentry/junior-amplitude`
Expand Down
2 changes: 2 additions & 0 deletions packages/junior-memory/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ exported types, tools, and tests are authoritative.
- `MEMORY_RECALL_MAX_VECTOR_DISTANCE` or
`recallMaxVectorDistance` configures the vector candidate threshold.
- Generate schema changes with `pnpm --filter @sentry/junior-memory db:generate`.
- Generate self-contained data migrations with
`pnpm --filter @sentry/junior-memory db:generate:data --name <name>`.

Follow `../../policies/data-redaction.md`, `../../policies/security.md`, and the
plugin contract in `../junior-plugin-api/README.md`.
2 changes: 2 additions & 0 deletions packages/junior-memory/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"scripts": {
"build": "tsup && tsc -p tsconfig.build.json --emitDeclarationOnly",
"db:generate": "pnpm exec drizzle-kit generate --config drizzle.config.ts",
"db:generate:data": "pnpm exec junior-migrations generate --config drizzle.config.ts --out migrations",
"lint": "oxlint --config ../junior/.oxlintrc.json --deny-warnings src tests tsup.config.ts",
"prepare": "pnpm run build",
"prepack": "pnpm run build",
Expand All @@ -39,6 +40,7 @@
"zod": "catalog:"
},
"devDependencies": {
"@sentry/junior-migrations": "workspace:*",
"@sentry/junior-testing": "workspace:*",
"@types/node": "^25.9.1",
"drizzle-kit": "catalog:",
Expand Down
42 changes: 42 additions & 0 deletions packages/junior-migrations/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# `@sentry/junior-migrations`

Junior's migration format extends a Drizzle Kit journal with TypeScript data
migrations. Every journal entry resolves to exactly one `<tag>.sql` or
`<tag>.ts` file. Drizzle Kit continues to own numbering and schema snapshots;
the Junior migration runner owns execution and exact per-entry tracking.

TypeScript migrations target a versioned capability API and must not import
Junior runtime internals. Every parser, transform, and migration-specific
helper belongs permanently in the migration file so application refactors or
deletions cannot break pending upgrades.

## Authoring

Generate schema migrations with the owning package's normal Drizzle command.
Generate data migrations through this package's wrapper:

```bash
junior-migrations generate \
--config drizzle.config.ts \
--out migrations \
--name backfill_actor
```

The wrapper asks Drizzle Kit to create a custom journal entry and snapshot,
then replaces the empty SQL file with a `MigrationV1` TypeScript scaffold.

## Compatibility

Drizzle Kit remains the supported authoring tool. Drizzle ORM's stock
`migrate()` function is not a supported executor for mixed journals because it
requires every entry to have a SQL file. Call `runMigrationJournal` instead and
provide the host database adapter, state adapter, and TypeScript loader. The
same database adapter drives the journal ledger and is exposed as
`context.database`, so migration files never own connection or driver setup.

The runner rejects runtime imports of application source, relative modules,
and unversioned `@sentry/junior` modules. Migrations may import the append-only
`@sentry/junior/migration-helpers/v1` surface for parsing primitives, adapters,
stores, and key resolution. One-off migration decisions and data transforms
must still remain in the journal entry. Add a new helper or capability version
rather than changing an existing contract.
47 changes: 47 additions & 0 deletions packages/junior-migrations/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
{
"name": "@sentry/junior-migrations",
"version": "0.101.0",
"private": false,
"publishConfig": {
"access": "public"
},
"type": "module",
"repository": {
"type": "git",
"url": "git+https://github.com/getsentry/junior.git",
"directory": "packages/junior-migrations"
},
"bin": {
"junior-migrations": "dist/cli.js"
},
"exports": {
".": {
"types": "./src/index.ts",
"default": "./dist/index.js"
}
},
"files": [
"dist",
"src"
],
"scripts": {
"build": "tsup && tsc -p tsconfig.build.json --emitDeclarationOnly",
"prepare": "pnpm run build",
"prepack": "pnpm run build",
"lint": "oxlint --config ../junior/.oxlintrc.json --deny-warnings src tests tsup.config.ts",
"test": "vitest run",
"typecheck": "tsc --noEmit"
},
"peerDependencies": {
"drizzle-kit": "catalog:"
},
Comment thread
cursor[bot] marked this conversation as resolved.
"devDependencies": {
"@types/node": "^25.9.1",
"drizzle-kit": "catalog:",
"drizzle-orm": "catalog:",
"oxlint": "^1.66.0",
"tsup": "^8.5.1",
"typescript": "^6.0.3",
"vitest": "^4.1.7"
}
}
24 changes: 24 additions & 0 deletions packages/junior-migrations/src/cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { parseArgs } from "node:util";
import { generateTypeScriptMigration } from "./generate";

const { positionals, values } = parseArgs({
allowPositionals: true,
options: {
config: { type: "string" },
name: { type: "string" },
out: { type: "string", default: "./migrations" },
},
});

if (positionals[0] !== "generate" || !values.config || !values.name) {
throw new Error(
"Usage: junior-migrations generate --config <path> --name <name> [--out <dir>]",
);
}

const path = await generateTypeScriptMigration({
configPath: values.config,
migrationsFolder: values.out,
name: values.name,
});
console.log(`Created TypeScript migration ${path}`);
68 changes: 68 additions & 0 deletions packages/junior-migrations/src/generate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { spawn } from "node:child_process";
import { readFile, rename, writeFile } from "node:fs/promises";
import { resolve } from "node:path";
import { readMigrationJournal } from "./journal";

/** Drizzle Kit inputs used to create one TypeScript migration entry. */
export interface GenerateTypeScriptMigrationOptions {
configPath: string;
cwd?: string;
migrationsFolder: string;
name: string;
}

function run(command: string, args: string[], cwd: string): Promise<void> {
return new Promise((resolvePromise, reject) => {
const child = spawn(command, args, { cwd, stdio: "inherit" });
child.on("error", reject);
child.on("exit", (code) => {
if (code === 0) {
resolvePromise();
} else {
reject(new Error(`${command} exited with code ${code ?? "unknown"}`));
}
});
});
}

function scaffold(): string {
return `import type { MigrationV1 } from "@sentry/junior-migrations";\n\nconst migration = {\n apiVersion: 1,\n async up(context) {\n void context;\n },\n} satisfies MigrationV1;\n\nexport default migration;\n`;
}

/** Create a journaled TypeScript migration through Drizzle Kit's custom generator. */
export async function generateTypeScriptMigration(
options: GenerateTypeScriptMigrationOptions,
): Promise<string> {
const cwd = resolve(options.cwd ?? process.cwd());
const folder = resolve(cwd, options.migrationsFolder);
const before = await readMigrationJournal(folder).catch(() => []);
await run(
"drizzle-kit",
[
"generate",
"--custom",
"--config",
options.configPath,
"--name",
options.name,
],
cwd,
);
const after = await readMigrationJournal(folder);
if (after.length !== before.length + 1) {
throw new Error("Drizzle Kit did not create exactly one migration entry");
}
const entry = after.at(-1);
if (!entry) {
throw new Error("Drizzle Kit did not create a migration entry");
}
const sqlPath = resolve(folder, `${entry.tag}.sql`);
const source = await readFile(sqlPath, "utf8");
if (!source.includes("Custom SQL migration file")) {
throw new Error(`Refusing to replace non-custom migration ${entry.tag}`);
}
const typescriptPath = resolve(folder, `${entry.tag}.ts`);
await rename(sqlPath, typescriptPath);
await writeFile(typescriptPath, scaffold(), "utf8");
return typescriptPath;
}
19 changes: 19 additions & 0 deletions packages/junior-migrations/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export { generateTypeScriptMigration } from "./generate";
export { readMigrationJournal, resolveMigrations } from "./journal";
export { runMigrationJournal } from "./runner";
export type { GenerateTypeScriptMigrationOptions } from "./generate";
export type { RunMigrationJournalOptions } from "./runner";
export type {
MigrationContextV1,
MigrationDatabaseAdapter,
MigrationJsonValue,
MigrationJournalEntry,
MigrationLockV1,
MigrationProgressV1,
MigrationRedisV1,
MigrationRunResult,
MigrationStateV1,
MigrationV1,
ResolvedMigration,
TypeScriptMigrationLoader,
} from "./types";
Loading
Loading