Repository navigation
migrate resend (C5, 0.21.0): move a Resend account and codebase to Brew - #61
thedogwiththedataonit wants to merge 6 commits into
Conversation
| const paths = (imported.variables ?? []).map((variable) => variable.path) | ||
| const subject = imported.subjectLine ?? input.subject | ||
| const setup = await brew.appEmails.upsert( | ||
| asSdkInput<UpsertAppEmailInput>({ | ||
| triggerKey: key, | ||
| source: { format: 'html', content: imported.html }, |
There was a problem hiding this comment.
importAndSetUp passes only imported.html into the app email. The import returns bare merge tags and their defaults in variables[].fallback, but this code uses those variables only for the report. Resend variable fallbacks and React prop defaults therefore disappear when a send omits the value. Preserve the returned fallbacks in the new trigger contract before marking the item done.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/migrate/import-setup.ts
Line: 136-141
Comment:
**Email defaults disappear**
`importAndSetUp` passes only `imported.html` into the app email. The import returns bare merge tags and their defaults in `variables[].fallback`, but this code uses those variables only for the report. Resend variable fallbacks and React prop defaults therefore disappear when a send omits the value. Preserve the returned fallbacks in the new trigger contract before marking the item done.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| brandId: row.brandId, | ||
| name: row.identity?.brandName ?? row.domain ?? null, | ||
| } | ||
| run.manifest.brandId = row.brandId |
There was a problem hiding this comment.
readBrew replaces the saved brandId without comparing it with the current destination. Re-running the manifest with another --brand or BREW_API_KEY keeps the first brand's completed items and saved cursors. Completed items are skipped, while remaining writes go to the new brand. Reject a changed destination before replacing the saved ID or using its progress.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/migrate/pipeline.ts
Line: 135
Comment:
**Resume uses another brand**
`readBrew` replaces the saved `brandId` without comparing it with the current destination. Re-running the manifest with another `--brand` or `BREW_API_KEY` keeps the first brand's completed items and saved cursors. Completed items are skipped, while remaining writes go to the new brand. Reject a changed destination before replacing the saved ID or using its progress.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| after = page.next | ||
| phase.cursor = after | ||
| run.persist() | ||
| } while (after !== null) | ||
| phase.status = 'done' | ||
| phase.cursor = null |
There was a problem hiding this comment.
contactsPhase advances its cursor and marks the phase done even when upsertMany reports failed rows. Brew can return per-row errors without throwing, so these failures bypass the catch block. A rerun then skips the completed phase. The segment and topic passes do the same. Keep failed pages retryable and report failure until those rows are resolved.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/migrate/phases.ts
Line: 469-474
Comment:
**Refused rows never retry**
`contactsPhase` advances its cursor and marks the phase done even when `upsertMany` reports failed rows. Brew can return per-row errors without throwing, so these failures bypass the catch block. A rerun then skips the completed phase. The segment and topic passes do the same. Keep failed pages retryable and report failure until those rows are resolved.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| contacts: page.rows.map((row) => ({ | ||
| email: row.email, | ||
| customFields: { [fieldName]: true }, | ||
| ...(row.unsubscribed === true ? { subscribed: false } : {}), | ||
| })), |
There was a problem hiding this comment.
Segments bypass subscription checks
segmentsPhase imports every member, including contacts whose unsubscribed value is null. The contacts phase deliberately leaves those people out, but this later pass creates them without subscribed: false, so they default to subscribed. Apply the same subscription-state filter here so segment migration does not undo that safeguard.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/migrate/phases.ts
Line: 594-598
Comment:
**Segments bypass subscription checks**
`segmentsPhase` imports every member, including contacts whose `unsubscribed` value is `null`. The contacts phase deliberately leaves those people out, but this later pass creates them without `subscribed: false`, so they default to subscribed. Apply the same subscription-state filter here so segment migration does not undo that safeguard.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| if (spread && kind === 'unknown') { | ||
| unsupported.push('...spread') | ||
| } |
There was a problem hiding this comment.
analyzeMessage flags a spread only when the message kind is unknown. A call such as send({ template: { id: 'receipt' }, ...options }) is cleared for rewriting even if options contains cc, attachments, or a different template. That can switch a send which still needs Resend to the Brew adapter. Treat unresolved spreads as a blocker even when an explicit template is present.
| if (spread && kind === 'unknown') { | |
| unsupported.push('...spread') | |
| } | |
| if (spread) { | |
| unsupported.push('...spread') | |
| } |
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/migrate/code/analyze.ts
Line: 271-273
Comment:
**Hidden send options pass**
`analyzeMessage` flags a spread only when the message kind is unknown. A call such as `send({ template: { id: 'receipt' }, ...options })` is cleared for rewriting even if `options` contains `cc`, `attachments`, or a different template. That can switch a send which still needs Resend to the Brew adapter. Treat unresolved spreads as a blocker even when an explicit template is present.
```suggestion
if (spread) {
unsupported.push('...spread')
}
```
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| if (run.apply && run.options.rewriteImports && rewritable.length > 0) { | ||
| rewriteImports(run, scan, rewritable, templateItems, rewritten) |
There was a problem hiding this comment.
The template map is refreshed only when another file needs rewriting. After the first migration rewrites a shared client, importing a newly added template leaves rewritable empty and never updates the map. Sends through that client then have no entry for the new template. Refresh the managed map whenever migrated templates change, even if no import needs rewriting.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/migrate/phase-code.ts
Line: 406-407
Comment:
**New templates miss the map**
The template map is refreshed only when another file needs rewriting. After the first migration rewrites a shared client, importing a newly added template leaves `rewritable` empty and never updates the map. Sends through that client then have no entry for the new template. Refresh the managed map whenever migrated templates change, even if no import needs rewriting.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| if (mapChange.action === 'kept-user-version') { | ||
| run.findings.push({ | ||
| code: 'MAP_FILE_KEPT', | ||
| severity: 'warning', | ||
| message: `${mapPath} was edited since the migration wrote it, so it was kept; check it maps every template id.`, | ||
| }) | ||
| } else if (mapChange.sha256 !== undefined) { |
There was a problem hiding this comment.
rewriteImports continues changing clients after the map returns kept-user-version. The retained map may lack the newly required template entries, but those clients are still marked done and their manual steps disappear. This can replace a working Resend client with a Brew client that cannot find its templates. Block those rewrites until the retained map is checked or merged.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/migrate/phase-code.ts
Line: 484-490
Comment:
**Clients use an unchecked map**
`rewriteImports` continues changing clients after the map returns `kept-user-version`. The retained map may lack the newly required template entries, but those clients are still marked done and their manual steps disappear. This can replace a working Resend client with a Brew client that cannot find its templates. Block those rewrites until the retained map is checked or merged.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| client: string | ||
| ): CallSiteRecord { | ||
| const { analysis } = site | ||
| const first = analysis.messages[0] as MessageAnalysis |
There was a problem hiding this comment.
Scanning resend.batch.send([]) produces an empty messages array. callSiteRecord takes its first entry anyway, then passes undefined to sendLine, which crashes on message.to. Handle an empty batch as an empty or manual call site so one such call does not abort the code scan.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/migrate/phase-code.ts
Line: 147
Comment:
**Empty batches stop scanning**
Scanning `resend.batch.send([])` produces an empty `messages` array. `callSiteRecord` takes its first entry anyway, then passes `undefined` to `sendLine`, which crashes on `message.to`. Handle an empty batch as an empty or manual call site so one such call does not abort the code scan.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| key, | ||
| component, | ||
| unsupported, | ||
| replacement: clean(run, replacement), |
There was a problem hiding this comment.
callSites[].replacement copies recipient and variable expressions into brew.migration.json. For example, to: 'alice@example.com' remains in the saved replacement because clean removes keys, not addresses or names. Users are told this file contains no personal data and is safe to commit. Replace literal values with placeholders before saving them.
How this was verified: Recipient source text is inserted into the replacement, the redactor leaves email literals unchanged, and the replacement is saved in the manifest.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/migrate/phase-code.ts
Line: 228
Comment:
**Manifest keeps personal data**
`callSites[].replacement` copies recipient and variable expressions into `brew.migration.json`. For example, `to: 'alice@example.com'` remains in the saved replacement because `clean` removes keys, not addresses or names. Users are told this file contains no personal data and is safe to commit. Replace literal values with placeholders before saving them.
**How this was verified:** Recipient source text is inserted into the replacement, the redactor leaves email literals unchanged, and the replacement is saved in the manifest.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| if (match !== undefined && item.brew?.audienceId !== match.audienceId) { | ||
| audienceName = `${name} (Resend)` | ||
| audienceId = rows.find( | ||
| (row) => row.audienceName === audienceName | ||
| )?.audienceId |
There was a problem hiding this comment.
Audience uses the wrong filter
ensureAudience checks ownership of the original name but blindly reuses an audience named <name> (Resend). If both names already exist, the fallback audience may select entirely different contacts. The migration still marks the segment or topic done. Check the fallback audience's saved ID and filter too, or choose another unused name.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/migrate/phases.ts
Line: 513-517
Comment:
**Audience uses the wrong filter**
`ensureAudience` checks ownership of the original name but blindly reuses an audience named `<name> (Resend)`. If both names already exist, the fallback audience may select entirely different contacts. The migration still marks the segment or topic done. Check the fallback audience's saved ID and filter too, or choose another unused name.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.7f77dd2 to
f49bcc4
Compare
d63cbcd to
fc6f0df
Compare
| if (domain === undefined) { | ||
| return | ||
| } | ||
| run.manifest.options.domain = domain |
There was a problem hiding this comment.
resolveTargetDomain replaces the saved domain without checking which domain owns the saved progress. If an apply stops partway through importing suppressions into domain A, rerunning with domain B starts from A's saved cursor. Earlier suppressions never reach B, yet the phase can finish successfully.
Bind progress to the resolved domain ID and reject a changed destination, or restart the domain-specific work.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/migrate/pipeline.ts
Line: 195
Comment:
**Retries skip unsubscribe rows**
`resolveTargetDomain` replaces the saved domain without checking which domain owns the saved progress. If an apply stops partway through importing suppressions into domain A, rerunning with domain B starts from A's saved cursor. Earlier suppressions never reach B, yet the phase can finish successfully.
Bind progress to the resolved domain ID and reject a changed destination, or restart the domain-specific work.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| if (input.moduleSyntax === 'cjs' && !input.typescript) { | ||
| return [...header, `exports.${MAP_EXPORT} = {`, ...lines, '}', ''].join( | ||
| '\n' | ||
| ) |
There was a problem hiding this comment.
If a JavaScript project has "type": "module" and a .cjs sender, mapFileContent writes exports.brewResendTemplates into brew-resend-map.js. Node treats that .js file as an ES module, so the rewritten sender cannot load its map.
Use a .cjs map for CommonJS content, or choose the map's format from its own package settings.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/migrate/code/rewrite.ts
Line: 261-264
Comment:
**Generated map cannot load**
If a JavaScript project has `"type": "module"` and a `.cjs` sender, `mapFileContent` writes `exports.brewResendTemplates` into `brew-resend-map.js`. Node treats that `.js` file as an ES module, so the rewritten sender cannot load its map.
Use a `.cjs` map for CommonJS content, or choose the map's format from its own package settings.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| ) | ||
| ) | ||
| // Pass 1: every contact's choices onto its record, page by page. | ||
| if (phase.counts?.passComplete !== 1) { |
There was a problem hiding this comment.
topicsPhase keeps passComplete after discovering new topics. After one apply completes the contact pass, a rerun can discover another topic but skip copying its choices. The later audience pass then marks that topic done even though no contacts received its field.
Reset the contact pass when the topic set changes, or track completion separately for each topic.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/migrate/phases.ts
Line: 839
Comment:
**New topic choices are skipped**
`topicsPhase` keeps `passComplete` after discovering new topics. After one apply completes the contact pass, a rerun can discover another topic but skip copying its choices. The later audience pass then marks that topic done even though no contacts received its field.
Reset the contact pass when the topic set changes, or track completion separately for each topic.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.f49bcc4 to
954239d
Compare
427b0b6 to
39b7ae4
Compare
d50df52 to
0aa3796
Compare
| const lineEnd = | ||
| requireAt === undefined | ||
| ? 0 | ||
| : text.indexOf('\n', requireAt.specifier.end) | ||
| const at = lineEnd === -1 ? text.length : lineEnd | ||
| edits.push({ | ||
| start: at, | ||
| end: at, | ||
| replacement: `${at === 0 ? '' : '\n'}const { ${MAP_EXPORT} } = require(${quote}${specifier}${quote})${at === 0 ? '\n' : ''}`, |
There was a problem hiding this comment.
Rewritten CommonJS senders cannot load
rewriteFile inserts the map's require after the whole line containing require('resend'). If that line also creates the client, as in const { Resend } = require('resend'); const resend = new Resend(process.env.RESEND_API_KEY), the rewritten constructor reads brewResendTemplates before its declaration. Loading the file then throws, breaking all sends through it. Insert the map declaration before any rewritten constructor.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/migrate/code/rewrite.ts
Line: 223-231
Comment:
**Rewritten CommonJS senders cannot load**
`rewriteFile` inserts the map's `require` after the whole line containing `require('resend')`. If that line also creates the client, as in `const { Resend } = require('resend'); const resend = new Resend(process.env.RESEND_API_KEY)`, the rewritten constructor reads `brewResendTemplates` before its declaration. Loading the file then throws, breaking all sends through it. Insert the map declaration before any rewritten constructor.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| export function rethrowIfFatal(ctx: CliContext, error: unknown): void { | ||
| ctx.signal.throwIfAborted() | ||
| if ( | ||
| (error instanceof BrewApiError || error instanceof CliApiError) && | ||
| (error.status === 401 || error.status === 403) | ||
| ) { | ||
| throw error | ||
| } |
There was a problem hiding this comment.
Key failures lose authentication instructions
If a Resend key is revoked during segment-member or topic-choice reads, the phase catches ResendAccessError and turns it into a generic failure. rethrowIfFatal only passes Brew authentication errors through, so the outer Resend handler never returns exit 3 or adds the replacement-key instructions. Users instead get exit 1 without that next step. Let ResendAccessError reach the existing authentication handler.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/migrate/run.ts
Line: 188-195
Comment:
**Key failures lose authentication instructions**
If a Resend key is revoked during segment-member or topic-choice reads, the phase catches `ResendAccessError` and turns it into a generic failure. `rethrowIfFatal` only passes Brew authentication errors through, so the outer Resend handler never returns exit 3 or adds the replacement-key instructions. Users instead get exit 1 without that next step. Let `ResendAccessError` reach the existing authentication handler.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.39b7ae4 to
a567e15
Compare
| edits.push({ | ||
| start: locations.importInsertAt, | ||
| end: locations.importInsertAt, | ||
| replacement: `${locations.importInsertAt === 0 ? '' : '\n'}import { ${MAP_EXPORT} } from ${quote}${specifier}${quote}${locations.importInsertAt === 0 ? '\n' : ''}`, |
There was a problem hiding this comment.
Rewritten imports break file loading
rewriteFile breaks valid one-line ESM files such as import { Resend } from 'resend'; const resend = new Resend(process.env.RESEND_API_KEY);. It inserts the map import after the original import but adds neither a semicolon nor a trailing newline. The result contains from './brew-resend-map' const resend, which cannot parse, so sends through that file stop working.
End the inserted import with a semicolon or newline before marking the file done.
| replacement: `${locations.importInsertAt === 0 ? '' : '\n'}import { ${MAP_EXPORT} } from ${quote}${specifier}${quote}${locations.importInsertAt === 0 ? '\n' : ''}`, | |
| replacement: `${locations.importInsertAt === 0 ? '' : '\n'}import { ${MAP_EXPORT} } from ${quote}${specifier}${quote}\n`, |
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/migrate/code/rewrite.ts
Line: 217
Comment:
**Rewritten imports break file loading**
`rewriteFile` breaks valid one-line ESM files such as `import { Resend } from 'resend'; const resend = new Resend(process.env.RESEND_API_KEY);`. It inserts the map import after the original import but adds neither a semicolon nor a trailing newline. The result contains `from './brew-resend-map' const resend`, which cannot parse, so sends through that file stop working.
End the inserted import with a semicolon or newline before marking the file done.
```suggestion
replacement: `${locations.importInsertAt === 0 ? '' : '\n'}import { ${MAP_EXPORT} } from ${quote}${specifier}${quote}\n`,
```
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.…ion) The bundled spec is #61's plus exactly the spec deltas of brew-v2 claude/clerk-email-created 12a02af5e3 over 0fed90fbf3 (the kit's Clerk description) and claude/supabase-send-email-hook c3da555e60 over 12a02af5e3: `GET|PUT /v1/integrations/{provider}/connection` (`getIntegrationConnection`, `connectIntegrationHook`; providers `supabase_auth` and `clerk`), the `IntegrationConnection*` schemas and `supabase_auth` in the integration catalog. No other operation changes. Generated types regenerated. Re-vendor from brew-v2 main once both merge. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
0aa3796 to
638fb38
Compare
e5a5052 to
83beb90
Compare
…ion) The bundled spec is #61's plus exactly the spec deltas of brew-v2 claude/clerk-email-created 12a02af5e3 over 0fed90fbf3 (the kit's Clerk description) and claude/supabase-send-email-hook c3da555e60 over 12a02af5e3: `GET|PUT /v1/integrations/{provider}/connection` (`getIntegrationConnection`, `connectIntegrationHook`; providers `supabase_auth` and `clerk`), the `IntegrationConnection*` schemas and `supabase_auth` in the integration catalog. No other operation changes. Generated types regenerated. Re-vendor from brew-v2 main once both merge. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
638fb38 to
51e3333
Compare
83beb90 to
dd52162
Compare
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| const body = { | ||
| contacts: page.rows.map((row) => ({ | ||
| email: row.email, | ||
| customFields: { [fieldName]: true }, | ||
| ...(row.unsubscribed === true ? { subscribed: false } : {}), | ||
| })), | ||
| } |
There was a problem hiding this comment.
Selected phases omit consent history
Running --only segments or --only topics can create subscribed contacts without recording where their consent came from. These phases omit the consent record that contactsPhase adds, even though they can run without it. Later inline marketing sends can warn that those contacts have no consent record.
Add import consent for subscribed rows in both phases so they keep the same consent history as a full migration.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/migrate/phases.ts
Line: 593-599
Comment:
**Selected phases omit consent history**
Running `--only segments` or `--only topics` can create subscribed contacts without recording where their consent came from. These phases omit the `consent` record that `contactsPhase` adds, even though they can run without it. Later inline marketing sends can warn that those contacts have no consent record.
Add import consent for subscribed rows in both phases so they keep the same consent history as a full migration.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.…o Brew Plan by default, `--apply` to execute (destructive class). The Resend key comes from RESEND_API_KEY or --resend-key-stdin, rides only requests to api.resend.com (paced to 2 a second, 429s waited out) and never reaches Brew, the terminal or the manifest. - Templates → app emails by key: imported with `placeholders: "resend"` and their variables' fallbacks, set up by key from the converted HTML, published where they were; the intermediate import is deleted. - React Email files → app emails with `props: "merge_tags"`. - Contacts → upsertMany (subscribed with an import consent record, unsubscribed stay unsubscribed, no state left out); unsubscribed ones and suppressions → `--domain`'s unsubscribe list. - Segments and topics → a boolean contact field each plus an audience; topic choices are read per contact. - Draft broadcasts → Brew draft emails (nothing is sent); sent ones, domains, webhooks (origin only) and automations are reported. - A Resend domain also in Brew gets the DKIM_SELECTOR_IN_USE advice (both sign with resend._domainkey) and a subdomain to verify instead. - The code scan finds every `emails.send` / `batch.send` (not those in comments, strings or regexes), says what does not carry over, and suggests `getBrew().send(key, payload)`; `--rewrite-imports` moves a file to `@brew.new/sdk/resend` only when every send through it is a migrated template. - `brew.migration.json`: ids, keys, counts and cursors (never an address, a name or a key), so an interrupted apply resumes. Every Brew write carries an idempotency key. - Sources implement `MigrationSource`; Resend is the only one. - The `brew-migrate-from-resend` skill; init installs it only where the project uses Resend. `lib/trigger-key.ts` reuses the one grammar in `lib/project/state.ts`. MSW tests cover the plan (no Brew writes, ≥500 ms between Resend requests, no PII or secret in the manifest), --dry-run, --only code without a key, the sending-only key (exit 3), --apply across every phase, resume from a saved cursor, and --rewrite-imports; every run checks stdout and stderr for the Resend key, the Brew key and a webhook token. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
package.json and both launchers go to 0.19.0 (`sync:launchers`). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…_RANGE The rewritten files import `@brew.new/sdk/resend` (SDK 13.2.0), but the install-sdk next step only appeared when no SDK was declared at all, so an app on 12.3 to 13.1 got no step and a build that cannot resolve the adapter. `hasSdk` now uses init's `reachesSdkRange`, the same threshold the init install step uses. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PUT /v1/app-emails/{triggerKey} `source` takes `props`, `placeholders`
and `variables` (GetBrew/brew-v2#2145 b2912ad9f7). The vendored spec is
28-line `AppEmailPutRequest.source` delta; types regenerated.
`app-emails upsert --input` already passed the modes through; with
`--source-file` it replaced `--input`'s whole `source`, dropping them.
The file now fills `format` and `content` and keeps the rest, and the
format falls back to `--input`'s `source.format` (stdin). New example:
`--source-file emails/order-shipped.tsx --input '{"source":{"props":"merge_tags"}}'`.
`migrate resend` names that command for a React Email file it leaves to
move by hand, so its props stay merge tags like the imported ones.
RELEASING: the API gate names #2066, #2064 and #2145.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A template or React Email file whose key's trigger already starts a published automation no setup made (409 APP_EMAIL_TRIGGER_IN_USE) is left `manual` with the API's message instead of failing the run: a re-run alone repeats the refusal, and the next run after the person unpublishes that automation sets it up (non-done items re-plan). - The #2064 and #2145 spec commits fold into #54's vendored #2169 spec; CHANGELOG, RELEASING and comments name brew-v2 #2169 @ 6f8ef50544. - Regenerated src/generated/skills.ts after the restack. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
51e3333 to
01d5e0e
Compare
dd52162 to
d976e43
Compare
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After main's published 0.18.0 the DX numbers move up one (#54 0.19.0, #55 0.20.0, #61 0.21.0, this PR 0.22.0), and the SDK's DX releases follow its published 13.3.0 and pending 13.4.0: adapters 13.6.0 (SDK_RANGE), connect 13.7.0. doctor --project's tests use those ranges. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Stacked on #55 (→ #54 → #53 → #44 → main
2c1deff). CLI 0.19.0 (C5 of the DX push):brew-cli migrate resend.Publish order (nothing here is published, merged or tagged)
send,fireByKey) and Release CLI 0.17.0 with registry SDK 13 #64 (appEmails, adapter subpaths) rebased onto SDK main (which already has the canvas revocation fix this CLI's main needs) and published. Numbers are final: init + the brew.new / create-brew launchers (C4, 0.20.0) #55 ships as 13.1.0 and Release CLI 0.17.0 with registry SDK 13 #64 as 13.2.0, after the canvas fix's 13.0.0; this CLI takes^13.2.0. Both SDK PRs are currently behind SDK main (init + the brew.new / create-brew launchers (C4, 0.20.0) #55 conflicts).bun add @brew.new/sdk@^<that version>(regeneratesbun.lockfrom the registry, replacing main's git pin), re-vendoropenapi/public-api-v1.yamlfrom brew-v2 main, frozen install + full checks, then CLI 0.17.0 → 0.18.0 → 0.19.0.brew.newandcreate-brewon npm (both still 404) for the token's account; until then the release step warns and skips them.What
migrate resendplans by default and writesbrew.migration.json;--dry-runwrites nothing;--apply(destructive class, exit 4 without--yes) executes and resumes when re-run.RESEND_API_KEYor--resend-key-stdin; it rides only requests toapi.resend.comthroughvendorFetch(never recorded in the Brew transport, redacted from errors), never reaches Brew, the terminal or the manifest. A sending-only key exits 3 (RESEND_KEY_SENDING_ONLY) with a full-access next step.retry-after); contacts, suppressions, segment members and topic choices page with saved cursors; every Brew write carries an idempotency key; done items are skipped on re-run.{{{VAR}}})POST /v1/emails/importwithplaceholders: "resend"+ variable fallbacks →PUT /v1/app-emails/{key}from the converted HTML (published where it was;fromcarried) → the intermediate import deletedprops: "merge_tags"(a file importing another component is manual)upsertMany: subscribed with an import consent record, unsubscribed withsubscribed: false(+--domain's unsubscribe list), no state left outresend_segment_*on members + an audience on itresend_topic_*(true = opted in) from each contact's choices (one request per contact; the plan estimates the time) + an audience--domain's unsubscribe list{{ trigger.* }}tag gets a warningDKIM_SELECTOR_IN_USEadvice (Brew also signs withresend._domainkey) and a subdomain to verify in Brewemails.send/batch.send(TypeScript-aware whentypescriptis installed, tokenizer otherwise; comments, strings and regexes are not calls), records each call site with what does not carry over (attachments,cc, rawhtml,scheduledAt, …) and a suggestedgetBrew().send(key, payload)as a blockingedit-filenext step.--rewrite-importsmoves a file to@brew.new/sdk/resendand writeslib/brew-resend-map.tsonly when every send through it is a migrated template.brew.migration.json: ids, keys, counts, cursors, call sites; never an address, a name or a key (tested).MigrationSourceinterface; Resend is the only source.brew-migrate-from-resendskill (embedded;initinstalls it only in projects that use Resend). README, brew-cli skill, CHANGELOG## 0.19.0, version 0.19.0 + launchers synced.app-emails upsert --inputpassessource.props/placeholders/variablesthrough; with--source-fileit used to replace--input's wholesourceand drop them, now the file fillsformat+contentand keeps the rest (format falls back to--input'ssource.formatfor stdin). New example--source-file emails/order-shipped.tsx --input '{"source":{"props":"merge_tags"}}';migrate resend's hand-move hint for a React Email file now includes it. Considered and kept:migrate resendstill imports then sets up rather than onePUTwithsource.placeholders: "resend", because the setup result carries no import notices (escaping_changed,dynamic_prop_folded,placeholder_unmapped, Tailwind drops) and no merge-path list, which the migration report shows per template.Checks (scratch SDK = SDK main + #64, head
68124a1)On
e5a5052:tsc✓ ·lint✓ ·test66 files / 941 tests ✓ · generated types match the vendored spec ✓ ·docs:commands:check,generate:skills:check,sync:launchers:check✓ ·build+ bin smoke ✓ ·smoke:launchers✓ ·smoke:signals✓.New MSW tests (
tests/commands/migrate-resend.test.ts,tests/helpers/resend-api.ts,tests/lib/migrate/keys.test.ts): plan with zero Brew writes and ≥ 500 ms between Resend requests; Resend key only on Resend requests, Brew key only on Brew's; no PII or secret in the manifest;--dry-run;--only codewithout a key; missing key exit 2;--resend-key-stdin; sending-only key exit 3;--applyexit 4 without--yes; full apply (exact import / upsert / contacts / audiences / unsubscribe bodies, idempotency keys, broadcast draft kept, imports deleted); resume from a saved cursor after a refused page;--rewrite-importson the fixture; key derivation. Every run checks stdout and stderr for the Resend key, the Brew key and a webhook token.Needs live validation (after deploy + publish)
migrate resendplan then--applyagainst a real Resend test account and a Brew test brand (examples/migrate-from-resend/before), including a React Email file with props, a broadcast draft, a topic, and the rewritten client sending through@brew.new/sdk/resend.🤖 Generated with Claude Code
Need help on this PR? Tag
@codesmith-botwith what you need.The PR is not ready to merge because the previous migration, rewrite and personal-data findings remain unresolved.
Fix with agent prompt
(2 more issues omitted due to length limits.)
Summary
Adds
migrate resendto plan and apply a resumable move from Resend to Brew.--input.Diagram
%%{init: {'theme': 'neutral'}}%% flowchart TD A[Read Resend account and project] --> B[Read Brew brand] B --> C[Plan and save progress] C --> D{Apply confirmed?} D -->|No| E[Return migration plan] D -->|Yes| F[Copy account data and email designs] F --> G[Optionally rewrite template clients] G --> H[Save progress and remaining steps]Reviews (7) · Last reviewed commit: "chore(release): migrate is 0.21.0; its i..." · Reviewed by Greptile