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
3 changes: 2 additions & 1 deletion skills/actor-runtime/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions src/services/builds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

/**
Expand Down Expand Up @@ -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
Expand Down
11 changes: 11 additions & 0 deletions src/services/dev-folder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
45 changes: 45 additions & 0 deletions test/integration/job-lifecycle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
});
Loading