From 6ad184cef15041121d327079257bc33546eb68b6 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 28 May 2026 17:22:54 +0000 Subject: [PATCH 1/7] Write git notes on Artifacts publish Attach JSON harness notes to published commits via refs/notes/commits when repo sessions publish, bootstrap source repos, or reconcile external pushes. Notes capture publish provenance, session/conversation ids, and check results without moving metadata out of D1. Also teach package_get_git_remote setup commands to fetch notes refs so local clones can read publish context. Co-authored-by: Kent C. Dodds --- .../capabilities/packages/get-git-remote.ts | 3 + .../src/repo/publish-git-notes.node.test.ts | 137 ++++++++ packages/worker/src/repo/publish-git-notes.ts | 309 ++++++++++++++++++ packages/worker/src/repo/repo-session-do.ts | 118 ++++++- 4 files changed, 566 insertions(+), 1 deletion(-) create mode 100644 packages/worker/src/repo/publish-git-notes.node.test.ts create mode 100644 packages/worker/src/repo/publish-git-notes.ts diff --git a/packages/worker/src/mcp/capabilities/packages/get-git-remote.ts b/packages/worker/src/mcp/capabilities/packages/get-git-remote.ts index a55f33eabc..86f6f6f23c 100644 --- a/packages/worker/src/mcp/capabilities/packages/get-git-remote.ts +++ b/packages/worker/src/mcp/capabilities/packages/get-git-remote.ts @@ -87,6 +87,9 @@ export const getGitRemoteCapability = defineDomainCapability( `git -c http.extraHeader=${shellQuote(gitExtraHeader)} clone ${shellQuote(info.remote)} ${shellQuote(cloneDirectory)}`, `cd ${shellQuote(cloneDirectory)}`, `git remote add kody ${shellQuote(info.remote)}`, + `git config remote.kody.fetch '+refs/heads/*:refs/remotes/kody/*'`, + `git config --add remote.kody.fetch '+refs/notes/*:refs/notes/*'`, + `git -c http.extraHeader=${shellQuote(gitExtraHeader)} fetch kody 'refs/notes/*:refs/notes/*'`, `git -c http.extraHeader=${shellQuote(gitExtraHeader)} push kody HEAD:${shellQuote(info.defaultBranch ?? 'main')}`, ], } diff --git a/packages/worker/src/repo/publish-git-notes.node.test.ts b/packages/worker/src/repo/publish-git-notes.node.test.ts new file mode 100644 index 0000000000..f9aabca72b --- /dev/null +++ b/packages/worker/src/repo/publish-git-notes.node.test.ts @@ -0,0 +1,137 @@ +import { expect, test, vi } from 'vitest' + +const mockGit = vi.hoisted(() => ({ + addNote: vi.fn(async () => 'note-commit-1'), + push: vi.fn(async () => ({ ok: true, refs: {} })), +})) + +vi.mock('isomorphic-git', () => ({ + default: mockGit, +})) + +vi.mock('isomorphic-git/http/web', () => ({ + default: {}, +})) + +vi.mock('@sentry/cloudflare', () => ({ + captureException: vi.fn(), +})) + +const { buildPublishGitNote, writeAndPushPublishGitNote } = await import( + './publish-git-notes.ts' +) + +test('buildPublishGitNote captures publish provenance and checks', () => { + const note = buildPublishGitNote({ + publishedBy: 'repo_session', + source: { + id: 'source-1', + entity_kind: 'package', + entity_id: 'pkg-1', + repo_id: 'package-pkg-1', + published_commit: 'commit-old', + }, + commit: 'commit-new', + publishedAt: '2026-05-28T12:00:00.000Z', + sessionId: 'session-1', + conversationId: 'conversation-1', + baseCommit: 'commit-old', + checks: { + runId: 'run-1', + treeHash: 'tree-1', + checkedAt: '2026-05-28T11:59:00.000Z', + ok: true, + results: [{ kind: 'lint', ok: true, message: 'ok' }], + }, + }) + + expect(note).toEqual({ + v: 1, + publishedAt: '2026-05-28T12:00:00.000Z', + publishedBy: 'repo_session', + sourceId: 'source-1', + entityKind: 'package', + entityId: 'pkg-1', + repoId: 'package-pkg-1', + commit: 'commit-new', + sessionId: 'session-1', + conversationId: 'conversation-1', + previousPublishedCommit: 'commit-old', + baseCommit: 'commit-old', + checks: { + runId: 'run-1', + treeHash: 'tree-1', + checkedAt: '2026-05-28T11:59:00.000Z', + ok: true, + results: [{ kind: 'lint', ok: true, message: 'ok' }], + }, + }) +}) + +test('writeAndPushPublishGitNote writes JSON note and pushes refs/notes/commits', async () => { + mockGit.addNote.mockClear() + mockGit.push.mockClear() + + const filesystem = { + readFile: vi.fn(async () => ''), + readFileBytes: vi.fn(async () => new Uint8Array()), + writeFile: vi.fn(async () => undefined), + writeFileBytes: vi.fn(async () => undefined), + rm: vi.fn(async () => undefined), + mkdir: vi.fn(async () => undefined), + readdir: vi.fn(async () => []), + stat: vi.fn(async () => ({ + type: 'file' as const, + size: 0, + mtime: new Date(), + })), + lstat: vi.fn(async () => ({ + type: 'file' as const, + size: 0, + mtime: new Date(), + })), + readlink: vi.fn(async () => ''), + symlink: vi.fn(async () => undefined), + } + + await writeAndPushPublishGitNote({ + filesystem, + dir: '/session', + commitOid: 'commit-new', + remote: 'https://example.com/repo.git', + token: 'token-1?expires=999', + remoteName: 'source', + note: buildPublishGitNote({ + publishedBy: 'external_push', + source: { + id: 'source-1', + entity_kind: 'job', + entity_id: 'job-1', + repo_id: 'job-job-1', + published_commit: null, + }, + commit: 'commit-new', + }), + }) + + expect(mockGit.addNote).toHaveBeenCalledWith( + expect.objectContaining({ + dir: '/session', + ref: 'refs/notes/commits', + oid: 'commit-new', + force: true, + }), + ) + expect(String(mockGit.addNote.mock.calls[0]?.[0]?.note)).toContain( + '"publishedBy": "external_push"', + ) + expect(mockGit.push).toHaveBeenCalledWith( + expect.objectContaining({ + dir: '/session', + remote: 'source', + url: 'https://example.com/repo.git', + ref: 'refs/notes/commits', + remoteRef: 'refs/notes/commits', + }), + ) +}) diff --git a/packages/worker/src/repo/publish-git-notes.ts b/packages/worker/src/repo/publish-git-notes.ts new file mode 100644 index 0000000000..6b6edacea1 --- /dev/null +++ b/packages/worker/src/repo/publish-git-notes.ts @@ -0,0 +1,309 @@ +import git from 'isomorphic-git' +import http from 'isomorphic-git/http/web' +import * as Sentry from '@sentry/cloudflare' +import { buildArtifactsGitAuth } from './artifacts.ts' +import { type EntityKind } from './types.ts' + +export const kodyPublishGitNoteVersion = 1 as const + +export type KodyPublishGitNotePublishedBy = + | 'repo_session' + | 'source_bootstrap' + | 'external_push' + +export type KodyPublishGitNoteChecks = { + runId: string + treeHash: string | null + checkedAt: string | null + ok: boolean + results: Array<{ + kind: string + ok: boolean + message: string + }> +} + +export type KodyPublishGitNote = { + v: typeof kodyPublishGitNoteVersion + publishedAt: string + publishedBy: KodyPublishGitNotePublishedBy + sourceId: string + entityKind: EntityKind + entityId: string + repoId: string + commit: string + sessionId?: string | null + conversationId?: string | null + previousPublishedCommit?: string | null + baseCommit?: string | null + checks?: KodyPublishGitNoteChecks | null +} + +export const kodyPublishGitNotesRef = 'refs/notes/commits' + +const kodyNoteAuthor = { + name: 'Kody', + email: 'kody@artifacts.local', +} + +type PublishGitNoteFileSystem = { + readFile(path: string): Promise + readFileBytes(path: string): Promise + writeFile(path: string, data: string): Promise + writeFileBytes(path: string, data: Uint8Array): Promise + rm(path: string, options?: { recursive?: boolean }): Promise + mkdir(path: string, options?: { recursive?: boolean }): Promise + readdir(path: string): Promise> + stat(path: string): Promise<{ + type: 'file' | 'directory' | 'symlink' + size: number + mtime: Date + mode?: number + }> + lstat(path: string): Promise<{ + type: 'file' | 'directory' | 'symlink' + size: number + mtime: Date + mode?: number + }> + readlink(path: string): Promise + symlink(target: string, path: string): Promise +} + +class GitStat { + readonly type: 'file' | 'directory' | 'symlink' + readonly size: number + readonly mtime: Date + readonly mtimeMs: number + readonly ctimeMs: number + readonly ino: number + readonly uid: number + readonly gid: number + readonly dev: number + readonly mode: number + + constructor(stat: { + type: 'file' | 'directory' | 'symlink' + size: number + mtime: Date + mode?: number + }) { + this.type = stat.type + this.size = stat.size + this.mtime = stat.mtime + this.mtimeMs = stat.mtime.getTime() + this.ctimeMs = this.mtimeMs + this.ino = 0 + this.uid = 0 + this.gid = 0 + this.dev = 0 + this.mode = + stat.mode ?? + (this.type === 'directory' + ? 16_777 + : this.type === 'symlink' + ? 40_960 + : 33_188) + } + + isFile() { + return this.type === 'file' + } + + isDirectory() { + return this.type === 'directory' + } + + isSymbolicLink() { + return this.type === 'symlink' + } +} + +function fsError(path: string, cause: unknown) { + if (cause instanceof Error && 'code' in cause && typeof cause.code === 'string') { + return cause + } + const err = new Error( + cause instanceof Error ? cause.message : `ENOENT: ${path}`, + ) as Error & { code: string } + err.code = 'ENOENT' + return err +} + +function createIsomorphicGitFs(filesystem: PublishGitNoteFileSystem) { + return { + promises: { + async readFile(path: string, options?: { encoding?: string } | string) { + const encoding = + typeof options === 'string' ? options : options?.encoding + try { + if (encoding === 'utf8' || encoding === 'utf-8') { + return await filesystem.readFile(path) + } + return await filesystem.readFileBytes(path) + } catch (error) { + throw fsError(path, error) + } + }, + async writeFile(path: string, data: string | Uint8Array) { + const parent = path.replace(/\/[^/]+$/, '') + if (parent && parent !== '/' && parent !== path) { + try { + await filesystem.mkdir(parent, { recursive: true }) + } catch { + // Parent may already exist. + } + } + if (typeof data === 'string') { + await filesystem.writeFile(path, data) + return + } + await filesystem.writeFileBytes(path, data) + }, + async unlink(path: string) { + try { + await filesystem.rm(path) + } catch (error) { + throw fsError(path, error) + } + }, + async readdir(path: string) { + return filesystem.readdir(path) + }, + async mkdir(path: string, mode?: { recursive?: boolean }) { + await filesystem.mkdir(path, { + recursive: typeof mode === 'object' ? mode.recursive : false, + }) + }, + async rmdir(path: string) { + await filesystem.rm(path) + }, + async stat(path: string) { + try { + return new GitStat(await filesystem.stat(path)) + } catch (error) { + throw fsError(path, error) + } + }, + async lstat(path: string) { + try { + return new GitStat(await filesystem.lstat(path)) + } catch (error) { + throw fsError(path, error) + } + }, + async readlink(path: string) { + try { + return await filesystem.readlink(path) + } catch (error) { + throw fsError(path, error) + } + }, + async symlink(target: string, path: string) { + await filesystem.symlink(target, path) + }, + async chmod(_path: string, _mode: number) {}, + }, + } +} + +export function buildPublishGitNote(input: { + publishedBy: KodyPublishGitNotePublishedBy + source: { + id: string + entity_kind: EntityKind + entity_id: string + repo_id: string + published_commit: string | null + } + commit: string + publishedAt?: string + sessionId?: string | null + conversationId?: string | null + baseCommit?: string | null + previousPublishedCommit?: string | null + checks?: KodyPublishGitNoteChecks | null +}): KodyPublishGitNote { + return { + v: kodyPublishGitNoteVersion, + publishedAt: input.publishedAt ?? new Date().toISOString(), + publishedBy: input.publishedBy, + sourceId: input.source.id, + entityKind: input.source.entity_kind, + entityId: input.source.entity_id, + repoId: input.source.repo_id, + commit: input.commit, + sessionId: input.sessionId ?? null, + conversationId: input.conversationId ?? null, + previousPublishedCommit: + input.previousPublishedCommit ?? input.source.published_commit, + baseCommit: input.baseCommit ?? null, + checks: input.checks ?? null, + } +} + +export async function writeAndPushPublishGitNote(input: { + filesystem: PublishGitNoteFileSystem + dir: string + commitOid: string + remote: string + token: string + remoteName?: string + note: KodyPublishGitNote +}) { + const fs = createIsomorphicGitFs(input.filesystem) + const noteText = `${JSON.stringify(input.note, null, 2)}\n` + await git.addNote({ + fs, + dir: input.dir, + ref: kodyPublishGitNotesRef, + oid: input.commitOid, + note: noteText, + force: true, + author: kodyNoteAuthor, + committer: kodyNoteAuthor, + }) + await git.push({ + fs, + http, + dir: input.dir, + remote: input.remoteName ?? 'source', + url: input.remote, + ref: kodyPublishGitNotesRef, + remoteRef: kodyPublishGitNotesRef, + onAuth() { + return buildArtifactsGitAuth({ token: input.token }) + }, + }) +} + +export async function attachPublishGitNoteBestEffort(input: { + filesystem: PublishGitNoteFileSystem + dir: string + commitOid: string + remote: string + token: string + remoteName?: string + note: KodyPublishGitNote + scope: string +}) { + try { + await writeAndPushPublishGitNote(input) + } catch (error) { + Sentry.captureException(error, { + tags: { scope: input.scope }, + extra: { + sourceId: input.note.sourceId, + commit: input.note.commit, + publishedBy: input.note.publishedBy, + }, + }) + console.warn('publish_git_note failed', { + scope: input.scope, + sourceId: input.note.sourceId, + commit: input.note.commit, + publishedBy: input.note.publishedBy, + error: error instanceof Error ? error.message : String(error), + }) + } +} diff --git a/packages/worker/src/repo/repo-session-do.ts b/packages/worker/src/repo/repo-session-do.ts index ed3f704901..1074e95c68 100644 --- a/packages/worker/src/repo/repo-session-do.ts +++ b/packages/worker/src/repo/repo-session-do.ts @@ -62,6 +62,11 @@ import { finalizePublishedEntitySource, publishFromExternalRef as publishExternalRefSource, } from './external-publish.ts' +import { + attachPublishGitNoteBestEffort, + buildPublishGitNote, + type KodyPublishGitNoteChecks, +} from './publish-git-notes.ts' import { type RepoGitCommand, type RepoRunCommandsResult, @@ -492,6 +497,60 @@ class RepoSessionBase extends DurableObject { ) } + private buildPublishGitNoteChecks( + checkStatus: RepoSessionCheckStatus, + ): KodyPublishGitNoteChecks | null { + if (!checkStatus.runId || checkStatus.ok == null) { + return null + } + return { + runId: checkStatus.runId, + treeHash: checkStatus.treeHash, + checkedAt: checkStatus.checkedAt, + ok: checkStatus.ok, + results: (checkStatus.results ?? []).map((entry) => ({ + kind: entry.kind, + ok: entry.ok, + message: entry.message, + })), + } + } + + private async attachSourcePublishGitNote(input: { + source: EntitySourceRow + commitOid: string + remote: string + token: string + remoteName?: string + publishedBy: 'repo_session' | 'source_bootstrap' | 'external_push' + sessionId?: string | null + conversationId?: string | null + baseCommit?: string | null + previousPublishedCommit?: string | null + checks?: KodyPublishGitNoteChecks | null + scope: string + }) { + await attachPublishGitNoteBestEffort({ + filesystem: this.fileSystem, + dir: repoSessionWorkspacePrefix, + commitOid: input.commitOid, + remote: input.remote, + token: input.token, + remoteName: input.remoteName, + note: buildPublishGitNote({ + publishedBy: input.publishedBy, + source: input.source, + commit: input.commitOid, + sessionId: input.sessionId, + conversationId: input.conversationId, + baseCommit: input.baseCommit, + previousPublishedCommit: input.previousPublishedCommit, + checks: input.checks, + }), + scope: input.scope, + }) + } + private async resolvePublishSource(input: { sessionId?: string sourceId?: string @@ -823,6 +882,17 @@ class RepoSessionBase extends DurableObject { ref: targetBranch, ...buildArtifactsGitAuth({ token: sourceAccess.token }), }) + await this.attachSourcePublishGitNote({ + source, + commitOid: publishedCommit, + remote: sourceAccess.remote, + token: sourceAccess.token, + remoteName: 'source', + publishedBy: 'source_bootstrap', + sessionId: input.sessionId, + previousPublishedCommit: null, + scope: 'repo.bootstrapSource.publish-git-note', + }) await updateEntitySource(this.env.APP_DB, { id: source.id, userId: source.user_id, @@ -1532,6 +1602,21 @@ class RepoSessionBase extends DurableObject { baseUrl: source.source_root, rebuildPackageArtifacts: input.rebuildPackageArtifacts ?? true, }) + const publishedCommit = sessionHeadCommit ?? sessionRow.base_commit + await this.attachSourcePublishGitNote({ + source, + commitOid: publishedCommit, + remote: sourceAccess.remote, + token: sourceAccess.token, + remoteName: 'source', + publishedBy: 'repo_session', + sessionId: sessionRow.id, + conversationId: sessionRow.conversation_id, + baseCommit: sessionRow.base_commit, + previousPublishedCommit: source.published_commit, + checks: this.buildPublishGitNoteChecks(checkStatus), + scope: 'repo.publishSession.publish-git-note', + }) await updateRepoSession(this.env.APP_DB, { id: sessionRow.id, userId: sessionRow.user_id, @@ -1569,6 +1654,10 @@ class RepoSessionBase extends DurableObject { repo: sourceRepo, scope: 'read', }) + const sourceWriteAccess = await ensureArtifactRepoRemote({ + repo: sourceRepo, + scope: 'write', + }) const targetBranch = sourceInfo?.defaultBranch ?? defaultSessionBranch if (input.expectedHead) { const currentHead = await resolveArtifactDefaultBranchHead({ @@ -1598,11 +1687,13 @@ class RepoSessionBase extends DurableObject { ref: input.newCommit, force: true, }) - return publishExternalRefSource({ + const runId = crypto.randomUUID() + const publishResult = await publishExternalRefSource({ env: this.env, sourceId: source.id, userId: input.userId, newCommit: input.newCommit, + runId, isFastForward: async ({ previousCommit }) => await this.isAncestorCommit({ ancestor: previousCommit, @@ -1622,6 +1713,31 @@ class RepoSessionBase extends DurableObject { rebuildPackageArtifacts: input.rebuildPackageArtifacts ?? true, expectedPackageScope: input.expectedPackageScope, }) + if (publishResult.status === 'published') { + await this.attachSourcePublishGitNote({ + source, + commitOid: input.newCommit, + remote: sourceWriteAccess.remote, + token: sourceWriteAccess.token, + remoteName: 'origin', + publishedBy: 'external_push', + sessionId: input.sessionId, + previousPublishedCommit: publishResult.previous_commit, + checks: { + runId, + treeHash: null, + checkedAt: new Date().toISOString(), + ok: true, + results: publishResult.checks.map((entry) => ({ + kind: entry.kind, + ok: entry.ok, + message: entry.message, + })), + }, + scope: 'repo.publishFromExternalRef.publish-git-note', + }) + } + return publishResult } } From b2099c239efbc3009ba668daec4e8812653a1ae8 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 28 May 2026 18:02:11 +0000 Subject: [PATCH 2/7] Add repo_show_publish_note capability Expose read path for Kody publish harness git notes on Artifacts commits. Supports source_id or package identity lookup, parses v1 JSON notes, and returns raw note text plus structured fields. Co-authored-by: Kent C. Dodds --- .../src/mcp/capabilities/repo/domain.ts | 2 + .../worker/src/mcp/capabilities/repo/index.ts | 1 + .../repo/repo-show-publish-note.node.test.ts | 142 ++++++++++++ .../repo/repo-show-publish-note.ts | 112 +++++++++ .../src/repo/publish-git-notes.node.test.ts | 19 ++ packages/worker/src/repo/publish-git-notes.ts | 216 +++++++++++++++++- 6 files changed, 491 insertions(+), 1 deletion(-) create mode 100644 packages/worker/src/mcp/capabilities/repo/repo-show-publish-note.node.test.ts create mode 100644 packages/worker/src/mcp/capabilities/repo/repo-show-publish-note.ts diff --git a/packages/worker/src/mcp/capabilities/repo/domain.ts b/packages/worker/src/mcp/capabilities/repo/domain.ts index b0aeaa5153..b68e539803 100644 --- a/packages/worker/src/mcp/capabilities/repo/domain.ts +++ b/packages/worker/src/mcp/capabilities/repo/domain.ts @@ -9,6 +9,7 @@ import { repoReadFileCapability } from './repo-read-file.ts' import { repoRebaseSessionCapability } from './repo-rebase-session.ts' import { repoRunChecksCapability } from './repo-run-checks.ts' import { repoRunCommandsCapability } from './repo-run-commands.ts' +import { repoShowPublishNoteCapability } from './repo-show-publish-note.ts' import { repoSearchCapability } from './repo-search.ts' import { repoTreeCapability } from './repo-tree.ts' import { repoWriteFileCapability } from './repo-write-file.ts' @@ -28,6 +29,7 @@ export const repoDomain = defineDomain({ repoSearchCapability, repoRunChecksCapability, repoGetCheckStatusCapability, + repoShowPublishNoteCapability, repoPublishSessionCapability, repoRebaseSessionCapability, repoDiscardSessionCapability, diff --git a/packages/worker/src/mcp/capabilities/repo/index.ts b/packages/worker/src/mcp/capabilities/repo/index.ts index 51ecac0fa8..955a40ed23 100644 --- a/packages/worker/src/mcp/capabilities/repo/index.ts +++ b/packages/worker/src/mcp/capabilities/repo/index.ts @@ -10,6 +10,7 @@ export { repoReadFileCapability } from './repo-read-file.ts' export { repoRebaseSessionCapability } from './repo-rebase-session.ts' export { repoRunCommandsCapability } from './repo-run-commands.ts' export { repoRunChecksCapability } from './repo-run-checks.ts' +export { repoShowPublishNoteCapability } from './repo-show-publish-note.ts' export { repoSearchCapability } from './repo-search.ts' export { repoTreeCapability } from './repo-tree.ts' export { repoWriteFileCapability } from './repo-write-file.ts' diff --git a/packages/worker/src/mcp/capabilities/repo/repo-show-publish-note.node.test.ts b/packages/worker/src/mcp/capabilities/repo/repo-show-publish-note.node.test.ts new file mode 100644 index 0000000000..79cc378b1f --- /dev/null +++ b/packages/worker/src/mcp/capabilities/repo/repo-show-publish-note.node.test.ts @@ -0,0 +1,142 @@ +import { beforeEach, expect, test, vi } from 'vitest' + +const mockModule = vi.hoisted(() => ({ + getEntitySourceById: vi.fn(), + resolveOwnedPackageSource: vi.fn(), + readPublishGitNoteFromArtifactsRepo: vi.fn(), +})) + +vi.mock('#worker/repo/entity-sources.ts', () => ({ + getEntitySourceById: (...args: Array) => + mockModule.getEntitySourceById(...args), +})) + +vi.mock('#mcp/capabilities/packages/resolve-package-source.ts', () => ({ + resolveOwnedPackageSource: (...args: Array) => + mockModule.resolveOwnedPackageSource(...args), +})) + +vi.mock('#worker/repo/publish-git-notes.ts', async () => { + const actual = await vi.importActual('#worker/repo/publish-git-notes.ts') + return { + ...(actual as object), + readPublishGitNoteFromArtifactsRepo: (...args: Array) => + mockModule.readPublishGitNoteFromArtifactsRepo(...args), + } +}) + +const { repoShowPublishNoteCapability } = await import('./repo-show-publish-note.ts') + +function createContext(userId = 'user-1') { + return { + env: { APP_DB: {} } as Env, + callerContext: { + baseUrl: 'https://heykody.dev', + user: { + userId, + email: `${userId}@example.com`, + displayName: userId, + }, + remoteConnectors: null, + storageContext: null, + repoContext: null, + }, + } +} + +const sampleSource = { + id: 'source-1', + user_id: 'user-1', + entity_kind: 'package' as const, + entity_id: 'package-1', + repo_id: 'package-package-1', + published_commit: 'commit-published', + indexed_commit: null, + manifest_path: 'package.json', + source_root: '/', + last_external_check_at: null, + created_at: '2026-05-28T00:00:00.000Z', + updated_at: '2026-05-28T00:00:00.000Z', +} + +const sampleNote = { + v: 1 as const, + publishedAt: '2026-05-28T12:00:00.000Z', + publishedBy: 'repo_session' as const, + sourceId: 'source-1', + entityKind: 'package' as const, + entityId: 'package-1', + repoId: 'package-package-1', + commit: 'commit-published', + sessionId: 'session-1', + conversationId: null, + previousPublishedCommit: null, + baseCommit: 'commit-base', + checks: null, +} + +// eslint-disable-next-line epic-web/prefer-dispose-in-tests -- shared hoisted mocks reset between tests. +beforeEach(() => { + for (const fn of Object.values(mockModule)) { + fn.mockReset() + } +}) + +test('repo_show_publish_note reads the note for a source id', async () => { + mockModule.getEntitySourceById.mockResolvedValue(sampleSource) + mockModule.readPublishGitNoteFromArtifactsRepo.mockResolvedValue({ + found: true, + commit: 'commit-published', + rawNote: JSON.stringify(sampleNote), + note: sampleNote, + }) + + const result = await repoShowPublishNoteCapability.handler( + { source_id: 'source-1' }, + createContext(), + ) + + expect(mockModule.readPublishGitNoteFromArtifactsRepo).toHaveBeenCalledWith({ + env: expect.objectContaining({ APP_DB: {} }), + repoId: 'package-package-1', + commitOid: 'commit-published', + }) + expect(result.found).toBe(true) + expect(result.note?.publishedBy).toBe('repo_session') +}) + +test('repo_show_publish_note resolves package identity and explicit commit', async () => { + mockModule.resolveOwnedPackageSource.mockResolvedValue({ + packageId: 'package-1', + kodyId: 'demo', + name: '@kent/demo', + source: sampleSource, + }) + mockModule.readPublishGitNoteFromArtifactsRepo.mockResolvedValue({ + found: false, + commit: 'commit-old', + rawNote: null, + note: null, + }) + + const result = await repoShowPublishNoteCapability.handler( + { kody_id: 'demo', commit: 'commit-old' }, + createContext(), + ) + + expect(result.found).toBe(false) + expect(result.commit).toBe('commit-old') +}) + +test('repo_show_publish_note rejects cross-user sources', async () => { + mockModule.getEntitySourceById.mockResolvedValue({ + ...sampleSource, + user_id: 'other-user', + }) + await expect( + repoShowPublishNoteCapability.handler( + { source_id: 'source-1' }, + createContext(), + ), + ).rejects.toThrow('Repo source was not found for this user.') +}) diff --git a/packages/worker/src/mcp/capabilities/repo/repo-show-publish-note.ts b/packages/worker/src/mcp/capabilities/repo/repo-show-publish-note.ts new file mode 100644 index 0000000000..01e361282f --- /dev/null +++ b/packages/worker/src/mcp/capabilities/repo/repo-show-publish-note.ts @@ -0,0 +1,112 @@ +import { z } from 'zod' +import { defineDomainCapability } from '#mcp/capabilities/define-domain-capability.ts' +import { capabilityDomainNames } from '#mcp/capabilities/domain-metadata.ts' +import { requireMcpUser } from '#mcp/capabilities/meta/require-user.ts' +import { resolveOwnedPackageSource } from '#mcp/capabilities/packages/resolve-package-source.ts' +import { getEntitySourceById } from '#worker/repo/entity-sources.ts' +import { + kodyPublishGitNoteSchema, + readPublishGitNoteFromArtifactsRepo, +} from '#worker/repo/publish-git-notes.ts' + +const inputSchema = z + .object({ + source_id: z + .string() + .min(1) + .optional() + .describe('Shared entity source id for any repo-backed entity.'), + package_id: z.string().min(1).optional(), + kody_id: z.string().min(1).optional(), + commit: z + .string() + .min(1) + .optional() + .describe( + 'Commit oid to inspect. Defaults to the source published_commit.', + ), + }) + .superRefine((value, ctx) => { + const count = + (value.source_id !== undefined ? 1 : 0) + + (value.package_id !== undefined ? 1 : 0) + + (value.kody_id !== undefined ? 1 : 0) + if (count !== 1) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ['source_id'], + message: + 'Provide exactly one of `source_id`, `package_id`, or `kody_id`.', + }) + } + }) + +const outputSchema = z.object({ + found: z.boolean(), + commit: z.string(), + source_id: z.string(), + repo_id: z.string(), + raw_note: z.string().nullable(), + note: kodyPublishGitNoteSchema.nullable(), +}) + +export const repoShowPublishNoteCapability = defineDomainCapability( + capabilityDomainNames.repo, + { + name: 'repo_show_publish_note', + description: + 'Read the Kody publish harness git note attached to a published Artifacts commit. Use this to inspect publish provenance, session/conversation ids, and check summaries without opening D1.', + keywords: [ + 'repo', + 'git', + 'notes', + 'publish', + 'audit', + 'provenance', + 'artifacts', + ], + readOnly: true, + idempotent: true, + destructive: false, + inputSchema, + outputSchema, + async handler(args, ctx) { + const user = requireMcpUser(ctx.callerContext) + const source = + args.source_id !== undefined + ? await getEntitySourceById(ctx.env.APP_DB, args.source_id) + : ( + await resolveOwnedPackageSource({ + db: ctx.env.APP_DB, + userId: user.userId, + args: { + package_id: args.package_id, + kody_id: args.kody_id, + }, + }) + ).source + if (!source || source.user_id !== user.userId) { + throw new Error('Repo source was not found for this user.') + } + const commit = args.commit ?? source.published_commit + if (!commit) { + throw new Error( + `Source "${source.id}" has no published commit yet. Pass an explicit \`commit\` or publish source first.`, + ) + } + const result = await readPublishGitNoteFromArtifactsRepo({ + env: ctx.env, + repoId: source.repo_id, + commitOid: commit, + }) + return { + found: result.found, + commit: result.commit, + source_id: source.id, + repo_id: source.repo_id, + raw_note: result.rawNote, + note: result.note, + } + }, + }, +) diff --git a/packages/worker/src/repo/publish-git-notes.node.test.ts b/packages/worker/src/repo/publish-git-notes.node.test.ts index f9aabca72b..7247f23133 100644 --- a/packages/worker/src/repo/publish-git-notes.node.test.ts +++ b/packages/worker/src/repo/publish-git-notes.node.test.ts @@ -68,6 +68,25 @@ test('buildPublishGitNote captures publish provenance and checks', () => { }) }) +test('parsePublishGitNote validates versioned publish notes', async () => { + const { parsePublishGitNote } = await import('./publish-git-notes.ts') + expect( + parsePublishGitNote( + JSON.stringify({ + v: 1, + publishedAt: '2026-05-28T12:00:00.000Z', + publishedBy: 'external_push', + sourceId: 'source-1', + entityKind: 'job', + entityId: 'job-1', + repoId: 'job-job-1', + commit: 'commit-new', + }), + )?.publishedBy, + ).toBe('external_push') + expect(parsePublishGitNote('not-json')).toBeNull() +}) + test('writeAndPushPublishGitNote writes JSON note and pushes refs/notes/commits', async () => { mockGit.addNote.mockClear() mockGit.push.mockClear() diff --git a/packages/worker/src/repo/publish-git-notes.ts b/packages/worker/src/repo/publish-git-notes.ts index 6b6edacea1..d6b89c8aac 100644 --- a/packages/worker/src/repo/publish-git-notes.ts +++ b/packages/worker/src/repo/publish-git-notes.ts @@ -1,7 +1,12 @@ import git from 'isomorphic-git' import http from 'isomorphic-git/http/web' import * as Sentry from '@sentry/cloudflare' -import { buildArtifactsGitAuth } from './artifacts.ts' +import { z } from 'zod' +import { + buildArtifactsGitAuth, + buildAuthenticatedArtifactsRemote, + resolveArtifactSourceRepo, +} from './artifacts.ts' import { type EntityKind } from './types.ts' export const kodyPublishGitNoteVersion = 1 as const @@ -41,6 +46,37 @@ export type KodyPublishGitNote = { export const kodyPublishGitNotesRef = 'refs/notes/commits' +export const kodyPublishGitNoteSchema = z.object({ + v: z.literal(kodyPublishGitNoteVersion), + publishedAt: z.string(), + publishedBy: z.enum(['repo_session', 'source_bootstrap', 'external_push']), + sourceId: z.string(), + entityKind: z.enum(['skill', 'app', 'job', 'package']), + entityId: z.string(), + repoId: z.string(), + commit: z.string(), + sessionId: z.string().nullable().optional(), + conversationId: z.string().nullable().optional(), + previousPublishedCommit: z.string().nullable().optional(), + baseCommit: z.string().nullable().optional(), + checks: z + .object({ + runId: z.string(), + treeHash: z.string().nullable(), + checkedAt: z.string().nullable(), + ok: z.boolean(), + results: z.array( + z.object({ + kind: z.string(), + ok: z.boolean(), + message: z.string(), + }), + ), + }) + .nullable() + .optional(), +}) + const kodyNoteAuthor = { name: 'Kody', email: 'kody@artifacts.local', @@ -307,3 +343,181 @@ export async function attachPublishGitNoteBestEffort(input: { }) } } + +type EphemeralGitWorkspace = { + fs: ReturnType + dir: string +} + +function createEphemeralGitWorkspace(): EphemeralGitWorkspace { + const store = new Map() + const textDecoder = new TextDecoder() + const textEncoder = new TextEncoder() + const filesystem: PublishGitNoteFileSystem = { + readFile: async (path) => { + const bytes = store.get(path) + if (!bytes) { + throw Object.assign(new Error(`ENOENT: ${path}`), { code: 'ENOENT' }) + } + return textDecoder.decode(bytes) + }, + readFileBytes: async (path) => { + const bytes = store.get(path) + if (!bytes) { + throw Object.assign(new Error(`ENOENT: ${path}`), { code: 'ENOENT' }) + } + return bytes + }, + writeFile: async (path, data) => { + store.set(path, textEncoder.encode(data)) + }, + writeFileBytes: async (path, data) => { + store.set(path, data) + }, + rm: async (path, options) => { + if (options?.recursive) { + for (const key of [...store.keys()]) { + if (key === path || key.startsWith(`${path}/`)) { + store.delete(key) + } + } + return + } + store.delete(path) + }, + mkdir: async (path, options) => { + if (options?.recursive) { + const parts = path.split('/').filter(Boolean) + let current = '' + for (const part of parts) { + current = `${current}/${part}` + store.set(current, new Uint8Array()) + } + return + } + store.set(path, new Uint8Array()) + }, + readdir: async (path) => { + const prefix = path.endsWith('/') ? path : `${path}/` + const names = new Set() + for (const key of store.keys()) { + if (!key.startsWith(prefix)) continue + const rest = key.slice(prefix.length) + const [name] = rest.split('/') + if (name) names.add(name) + } + return [...names] + }, + stat: async (path) => { + if (!store.has(path)) { + throw Object.assign(new Error(`ENOENT: ${path}`), { code: 'ENOENT' }) + } + const isDirectory = [...store.keys()].some( + (key) => key.startsWith(`${path}/`) && key !== path, + ) + return { + type: isDirectory ? ('directory' as const) : ('file' as const), + size: store.get(path)?.byteLength ?? 0, + mtime: new Date(), + } + }, + lstat: async (path) => filesystem.stat(path), + readlink: async () => { + throw new Error('readlink is not supported in ephemeral git workspace.') + }, + symlink: async (target, path) => { + await filesystem.writeFile(path, target) + }, + } + return { + fs: createIsomorphicGitFs(filesystem), + dir: '/repo', + } +} + +export function parsePublishGitNote(raw: string): KodyPublishGitNote | null { + const trimmed = raw.trim() + if (!trimmed) return null + try { + return kodyPublishGitNoteSchema.parse(JSON.parse(trimmed)) + } catch { + return null + } +} + +export async function readPublishGitNoteFromArtifactsRepo(input: { + env: Env + repoId: string + commitOid: string +}): Promise<{ + found: boolean + commit: string + rawNote: string | null + note: KodyPublishGitNote | null +}> { + const repo = await resolveArtifactSourceRepo(input.env, input.repoId) + const info = await repo.info() + if (!info?.remote) { + throw new Error('Artifact repo remote URL is unavailable.') + } + const token = await repo.createToken('read', 300) + const remote = buildAuthenticatedArtifactsRemote({ + remote: info.remote, + token: token.plaintext, + }) + const workspace = createEphemeralGitWorkspace() + const auth = buildArtifactsGitAuth({ token: token.plaintext }) + await git.clone({ + fs: workspace.fs, + http, + dir: workspace.dir, + url: remote, + depth: 1, + singleBranch: true, + ref: info.defaultBranch || 'main', + onAuth() { + return auth + }, + }) + try { + await git.fetch({ + fs: workspace.fs, + http, + dir: workspace.dir, + remote: 'origin', + ref: kodyPublishGitNotesRef, + onAuth() { + return auth + }, + }) + } catch { + return { + found: false, + commit: input.commitOid, + rawNote: null, + note: null, + } + } + try { + const noteBytes = await git.readNote({ + fs: workspace.fs, + dir: workspace.dir, + ref: kodyPublishGitNotesRef, + oid: input.commitOid, + }) + const rawNote = new TextDecoder().decode(noteBytes) + return { + found: true, + commit: input.commitOid, + rawNote, + note: parsePublishGitNote(rawNote), + } + } catch { + return { + found: false, + commit: input.commitOid, + rawNote: null, + note: null, + } + } +} From bb602e7a8fce65e78c31dcdb0bd6cf7c301c98a5 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 28 May 2026 18:10:39 +0000 Subject: [PATCH 3/7] Format publish git notes and repo_show_publish_note files Co-authored-by: Kent C. Dodds --- .../capabilities/repo/repo-show-publish-note.node.test.ts | 3 ++- packages/worker/src/repo/publish-git-notes.node.test.ts | 5 ++--- packages/worker/src/repo/publish-git-notes.ts | 6 +++++- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/worker/src/mcp/capabilities/repo/repo-show-publish-note.node.test.ts b/packages/worker/src/mcp/capabilities/repo/repo-show-publish-note.node.test.ts index 79cc378b1f..910a9e7de3 100644 --- a/packages/worker/src/mcp/capabilities/repo/repo-show-publish-note.node.test.ts +++ b/packages/worker/src/mcp/capabilities/repo/repo-show-publish-note.node.test.ts @@ -25,7 +25,8 @@ vi.mock('#worker/repo/publish-git-notes.ts', async () => { } }) -const { repoShowPublishNoteCapability } = await import('./repo-show-publish-note.ts') +const { repoShowPublishNoteCapability } = + await import('./repo-show-publish-note.ts') function createContext(userId = 'user-1') { return { diff --git a/packages/worker/src/repo/publish-git-notes.node.test.ts b/packages/worker/src/repo/publish-git-notes.node.test.ts index 7247f23133..a3f511bfcf 100644 --- a/packages/worker/src/repo/publish-git-notes.node.test.ts +++ b/packages/worker/src/repo/publish-git-notes.node.test.ts @@ -17,9 +17,8 @@ vi.mock('@sentry/cloudflare', () => ({ captureException: vi.fn(), })) -const { buildPublishGitNote, writeAndPushPublishGitNote } = await import( - './publish-git-notes.ts' -) +const { buildPublishGitNote, writeAndPushPublishGitNote } = + await import('./publish-git-notes.ts') test('buildPublishGitNote captures publish provenance and checks', () => { const note = buildPublishGitNote({ diff --git a/packages/worker/src/repo/publish-git-notes.ts b/packages/worker/src/repo/publish-git-notes.ts index d6b89c8aac..08e4da5a37 100644 --- a/packages/worker/src/repo/publish-git-notes.ts +++ b/packages/worker/src/repo/publish-git-notes.ts @@ -156,7 +156,11 @@ class GitStat { } function fsError(path: string, cause: unknown) { - if (cause instanceof Error && 'code' in cause && typeof cause.code === 'string') { + if ( + cause instanceof Error && + 'code' in cause && + typeof cause.code === 'string' + ) { return cause } const err = new Error( From a201a03dac900262f1ed76fea6d5c61aed989874 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 28 May 2026 18:33:19 +0000 Subject: [PATCH 4/7] Fix publish git note read error handling and directory mode Only treat missing notes ref / note as found:false. Propagate auth, network, and other git failures to callers instead of hiding them as false negatives. Also fix GitStat directory fallback mode typo (16_384 / 0o40000). Co-authored-by: Kent C. Dodds --- .../src/repo/publish-git-notes.node.test.ts | 75 ++++++++++++++++++- packages/worker/src/repo/publish-git-notes.ts | 25 ++++++- 2 files changed, 95 insertions(+), 5 deletions(-) diff --git a/packages/worker/src/repo/publish-git-notes.node.test.ts b/packages/worker/src/repo/publish-git-notes.node.test.ts index a3f511bfcf..933702867c 100644 --- a/packages/worker/src/repo/publish-git-notes.node.test.ts +++ b/packages/worker/src/repo/publish-git-notes.node.test.ts @@ -3,6 +3,9 @@ import { expect, test, vi } from 'vitest' const mockGit = vi.hoisted(() => ({ addNote: vi.fn(async () => 'note-commit-1'), push: vi.fn(async () => ({ ok: true, refs: {} })), + clone: vi.fn(async () => undefined), + fetch: vi.fn(async () => ({ ok: true, refs: {} })), + readNote: vi.fn(async () => new TextEncoder().encode('{"v":1}\n')), })) vi.mock('isomorphic-git', () => ({ @@ -17,8 +20,31 @@ vi.mock('@sentry/cloudflare', () => ({ captureException: vi.fn(), })) -const { buildPublishGitNote, writeAndPushPublishGitNote } = - await import('./publish-git-notes.ts') +vi.mock('./artifacts.ts', () => ({ + buildArtifactsGitAuth: vi.fn(() => ({ + username: 'x-access-token', + password: 'token', + })), + buildAuthenticatedArtifactsRemote: vi.fn( + (input: { remote: string; token: string }) => input.remote, + ), + resolveArtifactSourceRepo: vi.fn(async () => ({ + info: async () => ({ + remote: 'https://example.com/repo.git', + defaultBranch: 'main', + }), + createToken: async () => ({ + plaintext: 'token-1', + expiresAt: '2099-01-01T00:00:00.000Z', + }), + })), +})) + +const { + buildPublishGitNote, + readPublishGitNoteFromArtifactsRepo, + writeAndPushPublishGitNote, +} = await import('./publish-git-notes.ts') test('buildPublishGitNote captures publish provenance and checks', () => { const note = buildPublishGitNote({ @@ -153,3 +179,48 @@ test('writeAndPushPublishGitNote writes JSON note and pushes refs/notes/commits' }), ) }) + +test('readPublishGitNoteFromArtifactsRepo returns found false when notes ref is absent', async () => { + mockGit.clone.mockClear() + mockGit.fetch.mockClear() + mockGit.readNote.mockClear() + mockGit.fetch.mockRejectedValueOnce( + Object.assign(new Error('Could not find refs/notes/commits.'), { + code: 'NotFoundError', + }), + ) + + const result = await readPublishGitNoteFromArtifactsRepo({ + env: {} as Env, + repoId: 'package-pkg-1', + commitOid: 'commit-1', + }) + + expect(result.found).toBe(false) + expect(mockGit.readNote).not.toHaveBeenCalled() +}) + +test('readPublishGitNoteFromArtifactsRepo propagates fetch auth failures', async () => { + mockGit.fetch.mockRejectedValueOnce(new Error('HTTP Error: 401 Unauthorized')) + + await expect( + readPublishGitNoteFromArtifactsRepo({ + env: {} as Env, + repoId: 'package-pkg-1', + commitOid: 'commit-1', + }), + ).rejects.toThrow('401 Unauthorized') +}) + +test('readPublishGitNoteFromArtifactsRepo propagates readNote failures that are not missing-note errors', async () => { + mockGit.fetch.mockResolvedValueOnce({ ok: true, refs: {} }) + mockGit.readNote.mockRejectedValueOnce(new Error('filesystem corruption')) + + await expect( + readPublishGitNoteFromArtifactsRepo({ + env: {} as Env, + repoId: 'package-pkg-1', + commitOid: 'commit-1', + }), + ).rejects.toThrow('filesystem corruption') +}) diff --git a/packages/worker/src/repo/publish-git-notes.ts b/packages/worker/src/repo/publish-git-notes.ts index 08e4da5a37..a5fc03c1cf 100644 --- a/packages/worker/src/repo/publish-git-notes.ts +++ b/packages/worker/src/repo/publish-git-notes.ts @@ -136,7 +136,7 @@ class GitStat { this.mode = stat.mode ?? (this.type === 'directory' - ? 16_777 + ? 16_384 : this.type === 'symlink' ? 40_960 : 33_188) @@ -439,6 +439,19 @@ function createEphemeralGitWorkspace(): EphemeralGitWorkspace { } } +function isMissingPublishGitNoteError(error: unknown) { + if ( + error instanceof Error && + 'code' in error && + typeof error.code === 'string' && + error.code === 'NotFoundError' + ) { + return true + } + const message = error instanceof Error ? error.message : String(error) + return /refs\/notes\/commits|could not find|not found/i.test(message) +} + export function parsePublishGitNote(raw: string): KodyPublishGitNote | null { const trimmed = raw.trim() if (!trimmed) return null @@ -494,7 +507,10 @@ export async function readPublishGitNoteFromArtifactsRepo(input: { return auth }, }) - } catch { + } catch (error) { + if (!isMissingPublishGitNoteError(error)) { + throw error + } return { found: false, commit: input.commitOid, @@ -516,7 +532,10 @@ export async function readPublishGitNoteFromArtifactsRepo(input: { rawNote, note: parsePublishGitNote(rawNote), } - } catch { + } catch (error) { + if (!isMissingPublishGitNoteError(error)) { + throw error + } return { found: false, commit: input.commitOid, From 913b726df49c72550c61b94e4a936fd2561b02b6 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 28 May 2026 18:44:58 +0000 Subject: [PATCH 5/7] Fetch remote notes ref before push and force notes updates Subsequent publishes were silently failing when the local workspace lacked the remote notes history. Fetch refs/notes/commits best-effort, then push with force so note writes succeed after the first publish. Also defer source write token creation until external publish succeeds. Co-authored-by: Kent C. Dodds --- .../src/repo/publish-git-notes.node.test.ts | 9 +++++++ packages/worker/src/repo/publish-git-notes.ts | 24 +++++++++++++++---- packages/worker/src/repo/repo-session-do.ts | 8 +++---- 3 files changed, 33 insertions(+), 8 deletions(-) diff --git a/packages/worker/src/repo/publish-git-notes.node.test.ts b/packages/worker/src/repo/publish-git-notes.node.test.ts index 933702867c..2af160832f 100644 --- a/packages/worker/src/repo/publish-git-notes.node.test.ts +++ b/packages/worker/src/repo/publish-git-notes.node.test.ts @@ -169,6 +169,14 @@ test('writeAndPushPublishGitNote writes JSON note and pushes refs/notes/commits' expect(String(mockGit.addNote.mock.calls[0]?.[0]?.note)).toContain( '"publishedBy": "external_push"', ) + expect(mockGit.fetch).toHaveBeenCalledWith( + expect.objectContaining({ + dir: '/session', + remote: 'source', + url: 'https://example.com/repo.git', + ref: 'refs/notes/commits', + }), + ) expect(mockGit.push).toHaveBeenCalledWith( expect.objectContaining({ dir: '/session', @@ -176,6 +184,7 @@ test('writeAndPushPublishGitNote writes JSON note and pushes refs/notes/commits' url: 'https://example.com/repo.git', ref: 'refs/notes/commits', remoteRef: 'refs/notes/commits', + force: true, }), ) }) diff --git a/packages/worker/src/repo/publish-git-notes.ts b/packages/worker/src/repo/publish-git-notes.ts index a5fc03c1cf..757e37a3ac 100644 --- a/packages/worker/src/repo/publish-git-notes.ts +++ b/packages/worker/src/repo/publish-git-notes.ts @@ -292,6 +292,23 @@ export async function writeAndPushPublishGitNote(input: { note: KodyPublishGitNote }) { const fs = createIsomorphicGitFs(input.filesystem) + const remoteName = input.remoteName ?? 'source' + const onAuth = () => buildArtifactsGitAuth({ token: input.token }) + try { + await git.fetch({ + fs, + http, + dir: input.dir, + remote: remoteName, + url: input.remote, + ref: kodyPublishGitNotesRef, + onAuth, + }) + } catch (error) { + if (!isMissingPublishGitNoteError(error)) { + throw error + } + } const noteText = `${JSON.stringify(input.note, null, 2)}\n` await git.addNote({ fs, @@ -307,13 +324,12 @@ export async function writeAndPushPublishGitNote(input: { fs, http, dir: input.dir, - remote: input.remoteName ?? 'source', + remote: remoteName, url: input.remote, ref: kodyPublishGitNotesRef, remoteRef: kodyPublishGitNotesRef, - onAuth() { - return buildArtifactsGitAuth({ token: input.token }) - }, + force: true, + onAuth, }) } diff --git a/packages/worker/src/repo/repo-session-do.ts b/packages/worker/src/repo/repo-session-do.ts index 1074e95c68..c00a9826f7 100644 --- a/packages/worker/src/repo/repo-session-do.ts +++ b/packages/worker/src/repo/repo-session-do.ts @@ -1654,10 +1654,6 @@ class RepoSessionBase extends DurableObject { repo: sourceRepo, scope: 'read', }) - const sourceWriteAccess = await ensureArtifactRepoRemote({ - repo: sourceRepo, - scope: 'write', - }) const targetBranch = sourceInfo?.defaultBranch ?? defaultSessionBranch if (input.expectedHead) { const currentHead = await resolveArtifactDefaultBranchHead({ @@ -1714,6 +1710,10 @@ class RepoSessionBase extends DurableObject { expectedPackageScope: input.expectedPackageScope, }) if (publishResult.status === 'published') { + const sourceWriteAccess = await ensureArtifactRepoRemote({ + repo: sourceRepo, + scope: 'write', + }) await this.attachSourcePublishGitNote({ source, commitOid: input.newCommit, From 1ef923c17f4019b8f11188c20de0345f72b7f2e9 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 28 May 2026 19:04:36 +0000 Subject: [PATCH 6/7] Tighten missing-note detection and respect explicit null previous commit Only treat NotFoundError or notes-ref-specific messages as missing-note cases so errors like Repository not found propagate. Preserve explicit previousPublishedCommit: null for bootstrap notes. Co-authored-by: Kent C. Dodds --- .../src/repo/publish-git-notes.node.test.ts | 29 +++++++++++++++++++ packages/worker/src/repo/publish-git-notes.ts | 8 +++-- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/packages/worker/src/repo/publish-git-notes.node.test.ts b/packages/worker/src/repo/publish-git-notes.node.test.ts index 2af160832f..9fcd510143 100644 --- a/packages/worker/src/repo/publish-git-notes.node.test.ts +++ b/packages/worker/src/repo/publish-git-notes.node.test.ts @@ -93,6 +93,23 @@ test('buildPublishGitNote captures publish provenance and checks', () => { }) }) +test('buildPublishGitNote preserves explicit null previousPublishedCommit', () => { + const note = buildPublishGitNote({ + publishedBy: 'source_bootstrap', + source: { + id: 'source-1', + entity_kind: 'package', + entity_id: 'pkg-1', + repo_id: 'package-pkg-1', + published_commit: 'commit-stale', + }, + commit: 'commit-new', + previousPublishedCommit: null, + }) + + expect(note.previousPublishedCommit).toBeNull() +}) + test('parsePublishGitNote validates versioned publish notes', async () => { const { parsePublishGitNote } = await import('./publish-git-notes.ts') expect( @@ -221,6 +238,18 @@ test('readPublishGitNoteFromArtifactsRepo propagates fetch auth failures', async ).rejects.toThrow('401 Unauthorized') }) +test('readPublishGitNoteFromArtifactsRepo propagates repository-not-found fetch failures', async () => { + mockGit.fetch.mockRejectedValueOnce(new Error('Repository not found')) + + await expect( + readPublishGitNoteFromArtifactsRepo({ + env: {} as Env, + repoId: 'package-pkg-1', + commitOid: 'commit-1', + }), + ).rejects.toThrow('Repository not found') +}) + test('readPublishGitNoteFromArtifactsRepo propagates readNote failures that are not missing-note errors', async () => { mockGit.fetch.mockResolvedValueOnce({ ok: true, refs: {} }) mockGit.readNote.mockRejectedValueOnce(new Error('filesystem corruption')) diff --git a/packages/worker/src/repo/publish-git-notes.ts b/packages/worker/src/repo/publish-git-notes.ts index 757e37a3ac..9940e8defc 100644 --- a/packages/worker/src/repo/publish-git-notes.ts +++ b/packages/worker/src/repo/publish-git-notes.ts @@ -276,7 +276,9 @@ export function buildPublishGitNote(input: { sessionId: input.sessionId ?? null, conversationId: input.conversationId ?? null, previousPublishedCommit: - input.previousPublishedCommit ?? input.source.published_commit, + input.previousPublishedCommit !== undefined + ? input.previousPublishedCommit + : input.source.published_commit, baseCommit: input.baseCommit ?? null, checks: input.checks ?? null, } @@ -465,7 +467,9 @@ function isMissingPublishGitNoteError(error: unknown) { return true } const message = error instanceof Error ? error.message : String(error) - return /refs\/notes\/commits|could not find|not found/i.test(message) + return /refs\/notes\/commits|could not find refs\/notes\/commits/i.test( + message, + ) } export function parsePublishGitNote(raw: string): KodyPublishGitNote | null { From abeff468ed2261286e8850698116414bc41126bf Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 28 May 2026 19:11:15 +0000 Subject: [PATCH 7/7] Best-effort git note setup after external publish succeeds Wrap write-token creation and note attachment in try/catch so a token failure after finalizePublishedEntitySource does not fail the publish response. Co-authored-by: Kent C. Dodds --- packages/worker/src/repo/repo-session-do.ts | 68 +++++++++++++-------- 1 file changed, 42 insertions(+), 26 deletions(-) diff --git a/packages/worker/src/repo/repo-session-do.ts b/packages/worker/src/repo/repo-session-do.ts index c00a9826f7..85197180c3 100644 --- a/packages/worker/src/repo/repo-session-do.ts +++ b/packages/worker/src/repo/repo-session-do.ts @@ -1710,32 +1710,48 @@ class RepoSessionBase extends DurableObject { expectedPackageScope: input.expectedPackageScope, }) if (publishResult.status === 'published') { - const sourceWriteAccess = await ensureArtifactRepoRemote({ - repo: sourceRepo, - scope: 'write', - }) - await this.attachSourcePublishGitNote({ - source, - commitOid: input.newCommit, - remote: sourceWriteAccess.remote, - token: sourceWriteAccess.token, - remoteName: 'origin', - publishedBy: 'external_push', - sessionId: input.sessionId, - previousPublishedCommit: publishResult.previous_commit, - checks: { - runId, - treeHash: null, - checkedAt: new Date().toISOString(), - ok: true, - results: publishResult.checks.map((entry) => ({ - kind: entry.kind, - ok: entry.ok, - message: entry.message, - })), - }, - scope: 'repo.publishFromExternalRef.publish-git-note', - }) + try { + const sourceWriteAccess = await ensureArtifactRepoRemote({ + repo: sourceRepo, + scope: 'write', + }) + await this.attachSourcePublishGitNote({ + source, + commitOid: input.newCommit, + remote: sourceWriteAccess.remote, + token: sourceWriteAccess.token, + remoteName: 'origin', + publishedBy: 'external_push', + sessionId: input.sessionId, + previousPublishedCommit: publishResult.previous_commit, + checks: { + runId, + treeHash: null, + checkedAt: new Date().toISOString(), + ok: true, + results: publishResult.checks.map((entry) => ({ + kind: entry.kind, + ok: entry.ok, + message: entry.message, + })), + }, + scope: 'repo.publishFromExternalRef.publish-git-note', + }) + } catch (error) { + Sentry.captureException(error, { + tags: { scope: 'repo.publishFromExternalRef.publish-git-note-setup' }, + extra: { + sourceId: source.id, + commit: input.newCommit, + }, + }) + console.warn('publish_git_note setup failed', { + scope: 'repo.publishFromExternalRef.publish-git-note-setup', + sourceId: source.id, + commit: input.newCommit, + error: error instanceof Error ? error.message : String(error), + }) + } } return publishResult }