From 221aa2bdb348b46b44351ab8c3302cc5f0652c53 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 24 Sep 2026 12:21:54 +0000 Subject: [PATCH 1/2] feat: Announce the registered live dev folder in the build log A successful build of an Actor with a registered dev folder now ends with a runtime-formatted `[actor-runtime] Registered live dev folder: ...` line, so `apify push` no longer needs to print its own (unformatted) notice. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GJkSXC3g5BDYftW5SuNZM2 --- requirements/actor-driver.md | 2 ++ skills/actor-runtime/SKILL.md | 3 +- src/services/builds.ts | 7 ++++ src/services/dev-folder.ts | 11 +++++++ test/integration/job-lifecycle.test.ts | 45 ++++++++++++++++++++++++++ 5 files changed, 67 insertions(+), 1 deletion(-) diff --git a/requirements/actor-driver.md b/requirements/actor-driver.md index 4a09dc78..02660ca8 100644 --- a/requirements/actor-driver.md +++ b/requirements/actor-driver.md @@ -77,6 +77,8 @@ - **The mount is applied only when both a registered dev folder and a known working directory exist** for the run's resolved build; either missing means the run starts exactly as if the feature did not exist. +- A successful build of an Actor with a registered dev folder says so in its log, naming the folder and + the working directory runs will mount it over. - **A single run can opt out** of the mount (`?devFolder=false` on run start, `api.md`) without touching the registration. When a mount is skipped this way, the run's log says so. - The registration status the console and API report is the registered folder alone - never that a diff --git a/skills/actor-runtime/SKILL.md b/skills/actor-runtime/SKILL.md index f5a7855c..0f5ce475 100644 --- a/skills/actor-runtime/SKILL.md +++ b/skills/actor-runtime/SKILL.md @@ -86,7 +86,8 @@ build, so changing them needs an `apify push`. ## Iterate without rebuilding (dev folder) After that first `apify push`, the runtime registers the pushed directory as the Actor's **dev -folder** and bind-mounts it into every later run. Edit locally, recompile locally (`tsc`, or the +folder** (the build log's `Registered live dev folder` line confirms it) and bind-mounts it into every +later run. Edit locally, recompile locally (`tsc`, or the language equivalent), and `apify call` again - no `apify push`, no rebuild: ```sh diff --git a/src/services/builds.ts b/src/services/builds.ts index b9ea246b..f347bf8f 100644 --- a/src/services/builds.ts +++ b/src/services/builds.ts @@ -12,6 +12,7 @@ import { resolveDockerfileLocation } from './dockerfile-location.js'; import { resolveInputSchemaLocation } from './input-schema-location.js'; import { resolveActorMemorySettings } from './actor-memory.js'; import { appendLog, appendRuntimeLog, flushLog, markLogTerminal } from './logs.js'; +import { registeredDevFolderBuildLine } from './dev-folder.js'; import { isTerminalJobStatus, transitionJobStatus } from './job-status.js'; /** @@ -255,6 +256,12 @@ export async function runBuildInBackground( }, (chunk) => appendLog(record.id, chunk), ); + // Re-read: `apify push` registers the folder right before starting the build, and the console can + // change it mid-build. No working directory means no mount - the run log explains that instead. + const { localDevFolder } = (await getRegistries().actors.get(actor.id)) ?? actor; + if (localDevFolder && outcome.imageWorkingDirectory) { + appendRuntimeLog(record.id, registeredDevFolderBuildLine(localDevFolder, outcome.imageWorkingDirectory)); + } // Flush before writing the terminal status, not after (mirrors the same fix in // `services/runs.ts`'s `runInBackground`): by the time `driver.startBuild` resolves every `onLog` // call has already happened, so flushing here guarantees the persisted log is complete before a diff --git a/src/services/dev-folder.ts b/src/services/dev-folder.ts index 26dc45dd..2883da76 100644 --- a/src/services/dev-folder.ts +++ b/src/services/dev-folder.ts @@ -131,6 +131,17 @@ export function devFolderStatus(actor: ActorRecord): DevFolderStatus { return { localDevFolder: actor.localDevFolder ?? null }; } +/** Printed at the end of a successful build, so whoever pushed learns their folder is live without the + * CLI having to know anything about the runtime. */ +export function registeredDevFolderBuildLine(localDevFolder: string, imageWorkingDirectory: string): string { + return ( + `Registered live dev folder: ${localDevFolder}. Runs mount it over this build's working directory ` + + `${imageWorkingDirectory}, so local edits apply on the next \`apify call\` without another push. A compiled ` + + 'Actor (e.g. TypeScript) needs its local build first. Use `apify call --no-dev-folder` to run from the ' + + 'built image alone.' + ); +} + /** Printed when a registered dev folder cannot be mounted because the run's build has no working * directory (an image with no `WORKDIR`, or `/`). The run is unaffected; only the silence is. */ export function unknownWorkingDirectoryLine(localDevFolder: string): string { diff --git a/test/integration/job-lifecycle.test.ts b/test/integration/job-lifecycle.test.ts index b3089d16..1762e868 100644 --- a/test/integration/job-lifecycle.test.ts +++ b/test/integration/job-lifecycle.test.ts @@ -1022,3 +1022,48 @@ describe('startRun (public entry point) still creates a normal READY record the await waitForRunFinish(record.id, 5); }); }); + +describe('build log: the registered live dev folder', () => { + let server: TestServerHandle; + + afterEach(async () => { + await server.close(); + }); + + async function buildWith(localDevFolder: string | undefined, imageWorkingDirectory: string | undefined) { + server = await startTestServer(fixedBuildOutcomeDriver({ imageId: 'x', imageWorkingDirectory })); + const seeded = await seedActor(server, 'dev-folder-build-log-actor'); + // Registered after the caller's snapshot, like `apify push` does right before starting the build. + await getRegistries().actors.update(seeded.id, (current) => + current ? { ...current, localDevFolder } : current, + ); + const record: BuildRecord = { + id: generateId(), + userId: seeded.userId, + actorId: seeded.id, + versionNumber: '0.0', + buildNumber: '0.0.1', + tag: 'latest', + status: 'READY', + startedAt: new Date().toISOString(), + }; + await getRegistries().builds.set(record.id, record); + await runBuildInBackground(server.driver, seeded, VERSION, record, { tag: 'latest', useCache: true }); + return getFullLog(record.id); + } + + it('a successful build announces the registered folder as a runtime line', async () => { + const log = await buildWith('/home/me/actor', '/usr/src/app'); + const line = log.split('\n').find((l) => l.includes('Registered live dev folder')); + expect(line).toContain('[actor-runtime]'); + expect(line).toContain('/home/me/actor'); + expect(line).toContain('/usr/src/app'); + expect(line).toContain('apify call --no-dev-folder'); + }); + + it('says nothing without a registration, or without a working directory to mount over', async () => { + expect(await buildWith(undefined, '/usr/src/app')).not.toContain('Registered live dev folder'); + await server.close(); + expect(await buildWith('/home/me/actor', undefined)).not.toContain('Registered live dev folder'); + }); +}); From 33880a35fe22ffad9b526d5006c7bb34a3ec721e Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 25 Sep 2026 07:59:03 +0000 Subject: [PATCH 2/2] docs: Drop the requirements entry for the build-log dev-folder line Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GJkSXC3g5BDYftW5SuNZM2 --- requirements/actor-driver.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/requirements/actor-driver.md b/requirements/actor-driver.md index 02660ca8..4a09dc78 100644 --- a/requirements/actor-driver.md +++ b/requirements/actor-driver.md @@ -77,8 +77,6 @@ - **The mount is applied only when both a registered dev folder and a known working directory exist** for the run's resolved build; either missing means the run starts exactly as if the feature did not exist. -- A successful build of an Actor with a registered dev folder says so in its log, naming the folder and - the working directory runs will mount it over. - **A single run can opt out** of the mount (`?devFolder=false` on run start, `api.md`) without touching the registration. When a mount is skipped this way, the run's log says so. - The registration status the console and API report is the registered folder alone - never that a