diff --git a/skills/actor-runtime/SKILL.md b/skills/actor-runtime/SKILL.md index f5a7855..0f5ce47 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 b9ea246..f347bf8 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 26dc45d..2883da7 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 b3089d1..1762e86 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'); + }); +});