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
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ const mockModule = vi.hoisted(() => ({
getEntitySourceById: vi.fn(),
resolveArtifactSourceHead: vi.fn(),
publishFromExternalRef: vi.fn(),
listPublishedPackageArtifactTargets: vi.fn(),
rebuildPublishedPackageArtifact: vi.fn(),
getStaticPackageDependentsSummary: vi.fn(),
}))

Expand Down Expand Up @@ -36,6 +38,10 @@ vi.mock('#worker/repo/repo-session-do.ts', () => ({
repoSessionRpc: () => ({
publishFromExternalRef: (...args: Array<unknown>) =>
mockModule.publishFromExternalRef(...args),
listPublishedPackageArtifactTargets: (...args: Array<unknown>) =>
mockModule.listPublishedPackageArtifactTargets(...args),
rebuildPublishedPackageArtifact: (...args: Array<unknown>) =>
mockModule.rebuildPublishedPackageArtifact(...args),
}),
}))

Expand Down Expand Up @@ -80,6 +86,17 @@ beforeEach(() => {
recommended_next_action:
'No published bundle artifacts currently declare a static dependency on this package.',
})
mockModule.listPublishedPackageArtifactTargets.mockResolvedValue([])
mockModule.rebuildPublishedPackageArtifact.mockResolvedValue({
ok: true,
target: {
kind: 'module',
artifactName: '.',
entryPoint: 'src/index.ts',
bundleKind: 'module',
},
kvKey: 'bundle-key',
})
})

function createContext() {
Expand Down Expand Up @@ -133,15 +150,29 @@ test('publishes a new external Artifacts HEAD', async () => {
newCommit: 'commit-new',
expectedHead: 'commit-new',
allowForce: false,
rebuildPackageArtifacts: false,
}),
)
})

test('returns already_published when Artifacts HEAD matches D1', async () => {
const targets = [
{
kind: 'service',
artifactName: 'inbox',
entryPoint: 'src/service.ts',
bundleKind: 'module',
},
]
mockModule.resolveArtifactSourceHead.mockResolvedValue({
branch: 'main',
commit: 'commit-old',
})
mockModule.publishFromExternalRef.mockResolvedValue({
status: 'already_published',
published_commit: 'commit-old',
})
mockModule.listPublishedPackageArtifactTargets.mockResolvedValue(targets)

const result = await publishExternalPushCapability.handler(
{ package_id: 'package-1' },
Expand All @@ -160,7 +191,40 @@ test('returns already_published when Artifacts HEAD matches D1', async () => {
'No published bundle artifacts currently declare a static dependency on this package.',
},
})
expect(mockModule.publishFromExternalRef).not.toHaveBeenCalled()
expect(mockModule.publishFromExternalRef).toHaveBeenCalledWith(
expect.objectContaining({
sourceId: 'source-1',
newCommit: 'commit-old',
}),
)
expect(mockModule.rebuildPublishedPackageArtifact).toHaveBeenCalledWith({
sourceId: 'source-1',
userId: 'user-1',
publishedCommit: 'commit-old',
target: targets[0],
baseUrl: 'https://kody.test',
})
})

test('already_published without a commit fails instead of silently skipping artifact rebuild', async () => {
mockModule.resolveArtifactSourceHead.mockResolvedValue({
branch: 'main',
commit: 'commit-old',
})
mockModule.publishFromExternalRef.mockResolvedValue({
status: 'already_published',
published_commit: null,
})

await expect(
publishExternalPushCapability.handler(
{ package_id: 'package-1' },
createContext(),
),
).rejects.toThrow(
'already published, but no published commit is available to rebuild artifacts',
)
expect(mockModule.rebuildPublishedPackageArtifact).not.toHaveBeenCalled()
})

test('published output lists stale static dependents for the new dependency commit', async () => {
Expand Down Expand Up @@ -236,6 +300,67 @@ test('published output lists stale static dependents for the new dependency comm
)
})

test('rebuilds published package bundle artifacts one target at a time after publish', async () => {
const targets = [
{
kind: 'module',
artifactName: '.',
entryPoint: 'src/index.ts',
bundleKind: 'module',
},
{
kind: 'importable-module',
artifactName: '.',
entryPoint: 'src/index.ts',
bundleKind: 'importable-module',
},
]
mockModule.resolveArtifactSourceHead.mockResolvedValue({
branch: 'main',
commit: 'commit-new',
})
mockModule.publishFromExternalRef.mockResolvedValue({
status: 'published',
previous_commit: 'commit-old',
published_commit: 'commit-new',
manifest: {},
checks: [{ kind: 'manifest', ok: true, message: 'ok' }],
})
mockModule.listPublishedPackageArtifactTargets.mockResolvedValue(targets)

const result = await publishExternalPushCapability.handler(
{ package_id: 'package-1' },
createContext(),
)

expect(result.status).toBe('published')
expect(mockModule.listPublishedPackageArtifactTargets).toHaveBeenCalledWith({
sourceId: 'source-1',
userId: 'user-1',
})
expect(mockModule.rebuildPublishedPackageArtifact).toHaveBeenCalledTimes(2)
expect(mockModule.rebuildPublishedPackageArtifact).toHaveBeenNthCalledWith(
1,
{
sourceId: 'source-1',
userId: 'user-1',
publishedCommit: 'commit-new',
target: targets[0],
baseUrl: 'https://kody.test',
},
)
expect(mockModule.rebuildPublishedPackageArtifact).toHaveBeenNthCalledWith(
2,
{
sourceId: 'source-1',
userId: 'user-1',
publishedCommit: 'commit-new',
target: targets[1],
baseUrl: 'https://kody.test',
},
)
})

test('surfaces not-fast-forward refusal without force', async () => {
mockModule.resolveArtifactSourceHead.mockResolvedValue({
branch: 'main',
Expand Down Expand Up @@ -377,11 +502,16 @@ test('returns already_published when a retry observes that the reset attempt com
created_at: '2026-05-04T00:00:00.000Z',
updated_at: '2026-05-04T00:00:00.000Z',
})
mockModule.publishFromExternalRef.mockRejectedValueOnce(
new Error(
"Durable Object's isolate exceeded its memory limit and was reset",
),
)
mockModule.publishFromExternalRef
.mockRejectedValueOnce(
new Error(
"Durable Object's isolate exceeded its memory limit and was reset",
),
)
.mockResolvedValueOnce({
status: 'already_published',
published_commit: 'commit-new',
})

try {
const result = await publishExternalPushCapability.handler(
Expand All @@ -401,7 +531,7 @@ test('returns already_published when a retry observes that the reset attempt com
'No published bundle artifacts currently declare a static dependency on this package.',
},
})
expect(mockModule.publishFromExternalRef).toHaveBeenCalledTimes(1)
expect(mockModule.publishFromExternalRef).toHaveBeenCalledTimes(2)
} finally {
warnSpy.mockRestore()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
} from '#worker/package-runtime/static-package-dependents.ts'
import { repoSessionRpc } from '#worker/repo/repo-session-do.ts'
import { resolveArtifactSourceHead } from '#worker/repo/artifacts.ts'
import { rebuildPublishedPackageArtifactsViaRepoSession } from '#mcp/capabilities/repo/package-artifact-rebuild.ts'
import { resolveOwnedPackageSource } from './resolve-package-source.ts'

const inputSchema = z.object({
Expand Down Expand Up @@ -255,26 +256,13 @@ export const publishExternalPushCapability = defineDomainCapability(
kody_id: args.kody_id,
},
})
const publishedCommit = source.published_commit
const head = await resolveArtifactSourceHead(ctx.env, source.repo_id)
const newCommit = head.commit
if (!newCommit) {
throw new Error(
`Artifacts repo "${source.repo_id}" has no published HEAD to reconcile.`,
)
}
if (newCommit === publishedCommit) {
return {
status: 'already_published',
published_commit: publishedCommit,
static_dependents: await getPublishStaticDependents({
db: ctx.env.APP_DB,
userId: user.userId,
sourceId: source.id,
publishedCommit,
}),
} as const
}
const sessionId =
attemptIndex === 0
? `external-publish-${source.id}`
Expand All @@ -291,8 +279,22 @@ export const publishExternalPushCapability = defineDomainCapability(
expectedHead: newCommit,
allowForce: args.allow_force,
baseUrl: ctx.callerContext.baseUrl,
rebuildPackageArtifacts: false,
})
if (result.status === 'already_published') {
if (!result.published_commit) {
throw new Error(
`Package "${packageId}" is already published, but no published commit is available to rebuild artifacts.`,
)
}
Comment thread
cursor[bot] marked this conversation as resolved.
await rebuildPublishedPackageArtifactsViaRepoSession({
env: ctx.env,
rpcSessionId: sessionId,
sourceId: source.id,
userId: user.userId,
publishedCommit: result.published_commit,
baseUrl: ctx.callerContext.baseUrl,
})
return {
...result,
static_dependents: await getPublishStaticDependents({
Expand All @@ -306,6 +308,14 @@ export const publishExternalPushCapability = defineDomainCapability(
if (result.status !== 'published') {
return result
}
await rebuildPublishedPackageArtifactsViaRepoSession({
env: ctx.env,
rpcSessionId: sessionId,
sourceId: source.id,
userId: user.userId,
publishedCommit: result.published_commit,
baseUrl: ctx.callerContext.baseUrl,
})
return {
...result,
static_dependents: await getPublishStaticDependents({
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { repoSessionRpc } from '#worker/repo/repo-session-do.ts'

export async function rebuildPublishedPackageArtifactsViaRepoSession(input: {
env: Env
rpcSessionId: string
repoSessionId?: string
sourceId: string
userId: string
publishedCommit: string
baseUrl: string
}) {
try {
const session = repoSessionRpc(input.env, input.rpcSessionId)
const targets = await session.listPublishedPackageArtifactTargets({
sessionId: input.repoSessionId,
sourceId: input.sourceId,
userId: input.userId,
})
for (const target of targets) {
await session.rebuildPublishedPackageArtifact({
sessionId: input.repoSessionId,
sourceId: input.sourceId,
userId: input.userId,
publishedCommit: input.publishedCommit,
target,
baseUrl: input.baseUrl,
})
}
} catch (error) {
throw new Error(
`Package source publish succeeded, but bundle artifact rebuild failed for source "${input.sourceId}" at commit "${input.publishedCommit}". Re-run the publish capability to repair artifacts.`,
{ cause: error },
)
}
}
23 changes: 19 additions & 4 deletions packages/worker/src/mcp/capabilities/repo/repo-publish-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
repoPublishSessionOutputSchema,
repoSessionIdSchema,
} from './repo-shared.ts'
import { rebuildPublishedPackageArtifactsViaRepoSession } from './package-artifact-rebuild.ts'

export const repoPublishSessionCapability = defineDomainCapability(
capabilityDomainNames.repo,
Expand All @@ -21,14 +22,28 @@ export const repoPublishSessionCapability = defineDomainCapability(
outputSchema: repoPublishSessionOutputSchema,
async handler(args, ctx) {
const user = requireMcpUser(ctx.callerContext)
const result = await repoSessionRpc(
ctx.env,
args.session_id,
).publishSession({
const session = repoSessionRpc(ctx.env, args.session_id)
const sessionInfo = await session.getSessionInfo({
sessionId: args.session_id,
userId: user.userId,
})
const result = await session.publishSession({
sessionId: args.session_id,
userId: user.userId,
rebuildPackageArtifacts: false,
})
if (result.status === 'ok') {
if (sessionInfo.entity_type === 'package') {
await rebuildPublishedPackageArtifactsViaRepoSession({
env: ctx.env,
rpcSessionId: args.session_id,
repoSessionId: args.session_id,
sourceId: sessionInfo.source_id,
userId: user.userId,
publishedCommit: result.publishedCommit,
baseUrl: ctx.callerContext.baseUrl,
})
}
return {
status: 'ok' as const,
session_id: result.sessionId,
Expand Down
Loading
Loading