From e154a0e1db61f090d7e7df4013c1131af7d2df33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20Sol=C3=A1r?= Date: Wed, 14 Jan 2026 15:05:54 +0100 Subject: [PATCH 1/4] feat(actors): propagates title/description Propagates title and description from actor.json to the Apify Console. Updates the Actor's title and description when pushing if they differ from the current values. Adds tests to ensure that the title and description are correctly set when creating a new actor and updated on an existing actor. Fixes #723 --- src/commands/actors/push.ts | 22 ++++++++- test/api/commands/push.test.ts | 88 ++++++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+), 1 deletion(-) diff --git a/src/commands/actors/push.ts b/src/commands/actors/push.ts index 298c437e3..d426083af 100644 --- a/src/commands/actors/push.ts +++ b/src/commands/actors/push.ts @@ -185,6 +185,8 @@ export class ActorsPushCommand extends ApifyCommand { DEFAULT_RUN_OPTIONS) as ActorDefaultRunOptions; const newActor: ActorCollectionCreateOptions = { name: actorConfig!.name as string, + title: actorConfig!.title as string | undefined, + description: actorConfig!.description as string | undefined, defaultRunOptions, versions: [ { @@ -203,10 +205,28 @@ export class ActorsPushCommand extends ApifyCommand { } } + const actorClient = apifyClient.actor(actorId); + + // Update actor title/description if they differ from the current values + if (!isActorCreatedNow) { + const actorUpdates: Record = {}; + + if (actorConfig!.title && actorConfig!.title !== actor.title) { + actorUpdates.title = actorConfig!.title; + } + + if (actorConfig!.description && actorConfig!.description !== actor.description) { + actorUpdates.description = actorConfig!.description; + } + + if (Object.keys(actorUpdates).length > 0) { + await actorClient.update(actorUpdates); + } + } + info({ message: `Deploying Actor '${actorConfig!.name}' to Apify.` }); const filesSize = await sumFilesSizeInBytes(filePathsToPush, cwd); - const actorClient = apifyClient.actor(actorId); let sourceType; let sourceFiles; diff --git a/test/api/commands/push.test.ts b/test/api/commands/push.test.ts index 5f07c5bf2..bbceb6b6b 100644 --- a/test/api/commands/push.test.ts +++ b/test/api/commands/push.test.ts @@ -349,4 +349,92 @@ describe('[api] apify push', () => { }, TEST_TIMEOUT, ); + + it( + 'should set title and description when creating a new actor', + async () => { + toggleCwdBetweenFullAndParentPath(); + + const actorWithTitleName = `${ACTOR_NAME}-with-title`; + + await mkdir(joinCwdPath(actorWithTitleName), { recursive: true }); + forceNewCwd(actorWithTitleName); + + // Create an actor with title and description + const actorJson = { + actorSpecification: 1, + name: actorWithTitleName, + title: 'My Custom Actor Title', + description: 'This is a custom description for the actor.', + version: '0.0', + buildTag: 'latest', + }; + + writeFileSync(joinPath(LOCAL_CONFIG_PATH), JSON.stringify(actorJson, null, '\t'), { flag: 'w' }); + + await testRunCommand(ActorsPushCommand, { flags_noPrompt: true, flags_force: true }); + + const userInfo = await getLocalUserInfo(); + const actorId = `${userInfo.username}/${actorWithTitleName}`; + actorsForCleanup.add(actorId); + const createdActorClient = testUserClient.actor(actorId); + const createdActor = await createdActorClient.get(); + + expect(createdActor?.title).to.be.eql('My Custom Actor Title'); + expect(createdActor?.description).to.be.eql('This is a custom description for the actor.'); + + if (createdActor) await createdActorClient.delete(); + }, + TEST_TIMEOUT, + ); + + it( + 'should update title and description on existing actor', + async () => { + toggleCwdBetweenFullAndParentPath(); + + // Create an actor without title/description first + const testActorWithoutTitle = { + ...TEST_ACTOR, + name: `${ACTOR_NAME}-title-update`, + }; + const testActor = await testUserClient.actors().create(testActorWithoutTitle); + actorsForCleanup.add(testActor.id); + const testActorClient = testUserClient.actor(testActor.id); + + // Verify title/description are not set initially + const initialActor = await testActorClient.get(); + expect(initialActor?.title).to.not.be.eql('Updated Actor Title'); + + await mkdir(joinCwdPath(testActorWithoutTitle.name), { recursive: true }); + forceNewCwd(testActorWithoutTitle.name); + + // Create actor.json with title and description + const actorJson = { + actorSpecification: 1, + name: testActorWithoutTitle.name, + title: 'Updated Actor Title', + description: 'Updated actor description.', + version: '0.0', + buildTag: 'latest', + }; + + writeFileSync(joinPath(LOCAL_CONFIG_PATH), JSON.stringify(actorJson, null, '\t'), { flag: 'w' }); + + // Push to existing actor - this should update title and description + await testRunCommand(ActorsPushCommand, { + args_actorId: testActor.id, + flags_noPrompt: true, + flags_force: true, + }); + + const updatedActor = await testActorClient.get(); + + expect(updatedActor?.title).to.be.eql('Updated Actor Title'); + expect(updatedActor?.description).to.be.eql('Updated actor description.'); + + if (updatedActor) await testActorClient.delete(); + }, + TEST_TIMEOUT, + ); }); From a190d7451e5d9b947e20d784f777e3a4bcd1f1e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20Sol=C3=A1r?= Date: Thu, 15 Jan 2026 00:40:08 +0100 Subject: [PATCH 2/4] fix(actors): prevents overwriting actor data actor title and description in the console with local values. Only updates the title and description if they are not already present in the console. Ensures all attributes are provided for a successful update. Fixes #723 --- src/commands/actors/push.ts | 65 +++++++++++++++++++++++++++---------- 1 file changed, 48 insertions(+), 17 deletions(-) diff --git a/src/commands/actors/push.ts b/src/commands/actors/push.ts index d426083af..651bfe0a9 100644 --- a/src/commands/actors/push.ts +++ b/src/commands/actors/push.ts @@ -207,23 +207,6 @@ export class ActorsPushCommand extends ApifyCommand { const actorClient = apifyClient.actor(actorId); - // Update actor title/description if they differ from the current values - if (!isActorCreatedNow) { - const actorUpdates: Record = {}; - - if (actorConfig!.title && actorConfig!.title !== actor.title) { - actorUpdates.title = actorConfig!.title; - } - - if (actorConfig!.description && actorConfig!.description !== actor.description) { - actorUpdates.description = actorConfig!.description; - } - - if (Object.keys(actorUpdates).length > 0) { - await actorClient.update(actorUpdates); - } - } - info({ message: `Deploying Actor '${actorConfig!.name}' to Apify.` }); const filesSize = await sumFilesSizeInBytes(filePathsToPush, cwd); @@ -235,6 +218,54 @@ export class ActorsPushCommand extends ApifyCommand { const client = await actorClient.get(); if (!isActorCreatedNow) { + const actorUpdates: Record = {}; + + // Only update title if it is not present so we are sure we not + // overwrite user's edits in console by push. + + if (actorConfig!.title && !actor.title) { + actorUpdates.title = actorConfig!.title; + } + + // Only update description if it is not present so we are sure we not + // overwrite user's edits in console by push. + if (actorConfig!.description && !actor.description) { + actorUpdates.description = actorConfig!.description; + } + + // Provide all atributes needed for successfull update. + if (Object.keys(actorUpdates).length > 0 && client) { + const { + actorPermissionLevel, + actorStandby, + categories, + defaultRunOptions, + description, + isDeprecated, + isPublic, + name, + restartOnError, + seoDescription, + seoTitle, + title, + versions, + } = actor; + await actorClient.update({ + actorPermissionLevel, + actorStandby, + categories, + defaultRunOptions, + description: description ?? actorUpdates.description, + isDeprecated, + isPublic, + name, + restartOnError, + seoDescription, + seoTitle, + title: title ?? actorUpdates.title, + versions, + } as never); + } // Check when was files modified last const mostRecentModifiedFileMs = filePathsToPush.reduce((modifiedMs, filePath) => { const { mtimeMs, ctimeMs } = statSync(join(cwd, filePath)); From 20f3b6eef48adaad4752ab043f9a5309b7fc22aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20Sol=C3=A1r?= Date: Thu, 15 Jan 2026 11:12:12 +0100 Subject: [PATCH 3/4] fix(test): change order of tests and disable update --- src/commands/actors/push.ts | 48 -------------- test/api/commands/push.test.ts | 113 ++++++++------------------------- 2 files changed, 26 insertions(+), 135 deletions(-) diff --git a/src/commands/actors/push.ts b/src/commands/actors/push.ts index 651bfe0a9..953f7da6b 100644 --- a/src/commands/actors/push.ts +++ b/src/commands/actors/push.ts @@ -218,54 +218,6 @@ export class ActorsPushCommand extends ApifyCommand { const client = await actorClient.get(); if (!isActorCreatedNow) { - const actorUpdates: Record = {}; - - // Only update title if it is not present so we are sure we not - // overwrite user's edits in console by push. - - if (actorConfig!.title && !actor.title) { - actorUpdates.title = actorConfig!.title; - } - - // Only update description if it is not present so we are sure we not - // overwrite user's edits in console by push. - if (actorConfig!.description && !actor.description) { - actorUpdates.description = actorConfig!.description; - } - - // Provide all atributes needed for successfull update. - if (Object.keys(actorUpdates).length > 0 && client) { - const { - actorPermissionLevel, - actorStandby, - categories, - defaultRunOptions, - description, - isDeprecated, - isPublic, - name, - restartOnError, - seoDescription, - seoTitle, - title, - versions, - } = actor; - await actorClient.update({ - actorPermissionLevel, - actorStandby, - categories, - defaultRunOptions, - description: description ?? actorUpdates.description, - isDeprecated, - isPublic, - name, - restartOnError, - seoDescription, - seoTitle, - title: title ?? actorUpdates.title, - versions, - } as never); - } // Check when was files modified last const mostRecentModifiedFileMs = filePathsToPush.reduce((modifiedMs, filePath) => { const { mtimeMs, ctimeMs } = statSync(join(cwd, filePath)); diff --git a/test/api/commands/push.test.ts b/test/api/commands/push.test.ts index bbceb6b6b..dd133ca88 100644 --- a/test/api/commands/push.test.ts +++ b/test/api/commands/push.test.ts @@ -308,74 +308,27 @@ describe('[api] apify push', () => { await testActorClient.version(actorJson.version).update({ buildTag: 'beta' }); await testRunCommand(ActorsPushCommand, { args_actorId: testActor.id, flags_noPrompt: true }); + if (testActor) await testActorClient.delete(); expect(lastErrorMessage()).to.includes('is already on the platform'); }, TEST_TIMEOUT, ); - it( - 'should not push Actor when there are no files to push', - async () => { - toggleCwdBetweenFullAndParentPath(); - - await mkdir(joinCwdPath('empty-dir'), { recursive: true }); - - forceNewCwd('empty-dir'); - - await testRunCommand(ActorsPushCommand, { flags_noPrompt: true }); - - expect(lastErrorMessage()).to.include( - 'You need to call this command from a folder that has an Actor in it', - ); - }, - TEST_TIMEOUT, - ); - - it( - 'should not push when the folder does not seem to have a valid Actor', - async () => { - toggleCwdBetweenFullAndParentPath(); - - await mkdir(joinCwdPath('not-an-actor-i-promise'), { recursive: true }); - - forceNewCwd('not-an-actor-i-promise'); - - await writeFile(joinCwdPath('owo.txt'), 'Lorem ipsum'); - - await testRunCommand(ActorsPushCommand, { flags_noPrompt: true }); - - expect(lastErrorMessage()).to.include('A valid Actor could not be found in the current directory.'); - }, - TEST_TIMEOUT, - ); - it( 'should set title and description when creating a new actor', async () => { - toggleCwdBetweenFullAndParentPath(); - - const actorWithTitleName = `${ACTOR_NAME}-with-title`; - - await mkdir(joinCwdPath(actorWithTitleName), { recursive: true }); - forceNewCwd(actorWithTitleName); + const actorJson = JSON.parse(readFileSync(joinPath(LOCAL_CONFIG_PATH), 'utf8')); - // Create an actor with title and description - const actorJson = { - actorSpecification: 1, - name: actorWithTitleName, - title: 'My Custom Actor Title', - description: 'This is a custom description for the actor.', - version: '0.0', - buildTag: 'latest', - }; + actorJson.name = `${actorJson.name}-title-test`; + actorJson.title = 'My Custom Actor Title'; + actorJson.description = 'This is a custom description for the actor.'; writeFileSync(joinPath(LOCAL_CONFIG_PATH), JSON.stringify(actorJson, null, '\t'), { flag: 'w' }); - await testRunCommand(ActorsPushCommand, { flags_noPrompt: true, flags_force: true }); const userInfo = await getLocalUserInfo(); - const actorId = `${userInfo.username}/${actorWithTitleName}`; + const actorId = `${userInfo.username}/${actorJson.name}`; actorsForCleanup.add(actorId); const createdActorClient = testUserClient.actor(actorId); const createdActor = await createdActorClient.get(); @@ -389,51 +342,37 @@ describe('[api] apify push', () => { ); it( - 'should update title and description on existing actor', + 'should not push Actor when there are no files to push', async () => { toggleCwdBetweenFullAndParentPath(); - // Create an actor without title/description first - const testActorWithoutTitle = { - ...TEST_ACTOR, - name: `${ACTOR_NAME}-title-update`, - }; - const testActor = await testUserClient.actors().create(testActorWithoutTitle); - actorsForCleanup.add(testActor.id); - const testActorClient = testUserClient.actor(testActor.id); + await mkdir(joinCwdPath('empty-dir'), { recursive: true }); - // Verify title/description are not set initially - const initialActor = await testActorClient.get(); - expect(initialActor?.title).to.not.be.eql('Updated Actor Title'); + forceNewCwd('empty-dir'); - await mkdir(joinCwdPath(testActorWithoutTitle.name), { recursive: true }); - forceNewCwd(testActorWithoutTitle.name); + await testRunCommand(ActorsPushCommand, { flags_noPrompt: true }); - // Create actor.json with title and description - const actorJson = { - actorSpecification: 1, - name: testActorWithoutTitle.name, - title: 'Updated Actor Title', - description: 'Updated actor description.', - version: '0.0', - buildTag: 'latest', - }; + expect(lastErrorMessage()).to.include( + 'You need to call this command from a folder that has an Actor in it', + ); + }, + TEST_TIMEOUT, + ); - writeFileSync(joinPath(LOCAL_CONFIG_PATH), JSON.stringify(actorJson, null, '\t'), { flag: 'w' }); + it( + 'should not push when the folder does not seem to have a valid Actor', + async () => { + toggleCwdBetweenFullAndParentPath(); - // Push to existing actor - this should update title and description - await testRunCommand(ActorsPushCommand, { - args_actorId: testActor.id, - flags_noPrompt: true, - flags_force: true, - }); + await mkdir(joinCwdPath('not-an-actor-i-promise'), { recursive: true }); - const updatedActor = await testActorClient.get(); + forceNewCwd('not-an-actor-i-promise'); - expect(updatedActor?.title).to.be.eql('Updated Actor Title'); - expect(updatedActor?.description).to.be.eql('Updated actor description.'); + await writeFile(joinCwdPath('owo.txt'), 'Lorem ipsum'); + + await testRunCommand(ActorsPushCommand, { flags_noPrompt: true }); - if (updatedActor) await testActorClient.delete(); + expect(lastErrorMessage()).to.include('A valid Actor could not be found in the current directory.'); }, TEST_TIMEOUT, ); From d41d36f9585f43df188233f507e7c283be1f7bc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20Sol=C3=A1r?= Date: Thu, 15 Jan 2026 11:32:05 +0100 Subject: [PATCH 4/4] chore(tests): add test to check to not overwrite title and desc --- test/api/commands/push.test.ts | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/test/api/commands/push.test.ts b/test/api/commands/push.test.ts index dd133ca88..7ea725077 100644 --- a/test/api/commands/push.test.ts +++ b/test/api/commands/push.test.ts @@ -341,6 +341,37 @@ describe('[api] apify push', () => { TEST_TIMEOUT, ); + it( + 'should not rewrite current Actor title and description', + async () => { + const testActorWithTitleDesc = { + ...TEST_ACTOR, + title: 'Original Title', + description: 'Original description.', + }; + let testActor = await testUserClient.actors().create(testActorWithTitleDesc); + actorsForCleanup.add(testActor.id); + const testActorClient = testUserClient.actor(testActor.id); + + // Remove title and description from local actor.json + const actorJson = JSON.parse(readFileSync(joinPath(LOCAL_CONFIG_PATH), 'utf8')); + delete actorJson.title; + delete actorJson.description; + writeFileSync(joinPath(LOCAL_CONFIG_PATH), JSON.stringify(actorJson, null, '\t'), { flag: 'w' }); + + await testRunCommand(ActorsPushCommand, { args_actorId: testActor.id, flags_noPrompt: true }); + + testActor = (await testActorClient.get())!; + + if (testActor) await testActorClient.delete(); + + // Title and description should be preserved from the original actor + expect(testActor.title).to.be.eql('Original Title'); + expect(testActor.description).to.be.eql('Original description.'); + }, + TEST_TIMEOUT, + ); + it( 'should not push Actor when there are no files to push', async () => {