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
44 changes: 38 additions & 6 deletions docs/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -753,12 +753,28 @@ DESCRIPTION
Manages Actor build processes and versioning.

SUBCOMMANDS
builds rm Permanently removes an Actor build from the Apify
platform.
builds ls Lists all builds of the Actor.
builds log Prints the log of a specific build.
builds info Prints information about a specific build.
builds create Creates a new build of the Actor.
builds add-tag Adds a tag to a specific Actor build.
builds remove-tag Removes a tag from a specific Actor build.
builds rm Permanently removes an Actor build from
the Apify platform.
builds ls Lists all builds of the Actor.
builds log Prints the log of a specific build.
builds info Prints information about a specific build.
builds create Creates a new build of the Actor.
```

##### `apify builds add-tag`

```sh
DESCRIPTION
Adds a tag to a specific Actor build.

USAGE
$ apify builds add-tag -b <value> -t <value>

FLAGS
-b, --build=<value> The build ID to tag.
-t, --tag=<value> The tag to add to the build.
```

##### `apify builds create` / `apify actors build`
Expand Down Expand Up @@ -837,6 +853,22 @@ FLAGS
--offset=<value> Number of builds that will be skipped.
```

##### `apify builds remove-tag`

```sh
DESCRIPTION
Removes a tag from a specific Actor build.

USAGE
$ apify builds remove-tag -b <value> -t <value> [-y]

FLAGS
-b, --build=<value> The build ID to remove the tag from.
-t, --tag=<value> The tag to remove from the build.
-y, --yes Automatic yes to prompts; assume "yes"
as answer to all prompts.
```

##### `apify builds rm`

```sh
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@
"@skyra/jaro-winkler": "^1.1.1",
"adm-zip": "~0.5.15",
"ajv": "~8.17.1",
"apify-client": "^2.14.0",
"apify-client": "~2.22.0",
"archiver": "~7.0.1",
"axios": "^1.11.0",
"chalk": "~5.6.0",
Expand Down
2 changes: 2 additions & 0 deletions scripts/generate-cli-docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,12 @@ const categories: Record<string, CommandsInCategory[]> = {
'actor-build': [
//
{ command: Commands.builds },
{ command: Commands.buildsAddTag },
{ command: Commands.buildsCreate, aliases: [Commands.actorsBuild] },
{ command: Commands.buildsInfo },
{ command: Commands.buildsLog },
{ command: Commands.buildsLs },
{ command: Commands.buildsRemoveTag },
{ command: Commands.buildsRm },
],
'actor-run': [
Expand Down
4 changes: 4 additions & 0 deletions src/commands/builds/_index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { ApifyCommand } from '../../lib/command-framework/apify-command.js';
import { BuildsAddTagCommand } from './add-tag.js';
import { BuildsCreateCommand } from './create.js';
import { BuildsInfoCommand } from './info.js';
import { BuildsLogCommand } from './log.js';
import { BuildsLsCommand } from './ls.js';
import { BuildsRemoveTagCommand } from './remove-tag.js';
import { BuildsRmCommand } from './rm.js';

export class BuildsIndexCommand extends ApifyCommand<typeof BuildsIndexCommand> {
Expand All @@ -12,6 +14,8 @@ export class BuildsIndexCommand extends ApifyCommand<typeof BuildsIndexCommand>

static override subcommands = [
//
BuildsAddTagCommand,
BuildsRemoveTagCommand,
BuildsRmCommand,
BuildsLsCommand,
BuildsLogCommand,
Expand Down
92 changes: 92 additions & 0 deletions src/commands/builds/add-tag.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import type { ActorTaggedBuild, ApifyApiError } from 'apify-client';
import chalk from 'chalk';

import { ApifyCommand } from '../../lib/command-framework/apify-command.js';
import { Flags } from '../../lib/command-framework/flags.js';
import { error, success, warning } from '../../lib/outputs.js';
import { getLoggedClientOrThrow } from '../../lib/utils.js';

export class BuildsAddTagCommand extends ApifyCommand<typeof BuildsAddTagCommand> {
static override name = 'add-tag' as const;

static override description = 'Adds a tag to a specific Actor build.';

static override flags = {
build: Flags.string({
char: 'b',
description: 'The build ID to tag.',
required: true,
}),
tag: Flags.string({
char: 't',
description: 'The tag to add to the build.',
required: true,
}),
};

async run() {
const { build: buildId, tag } = this.flags;

const apifyClient = await getLoggedClientOrThrow();

const build = await apifyClient.build(buildId).get();

if (!build) {
error({ message: `Build with ID "${buildId}" was not found on your account.`, stdout: true });
return;
}

if (build.status !== 'SUCCEEDED') {
error({
message: `Build with ID "${buildId}" has status "${build.status}". Only successful builds can be tagged.`,
stdout: true,
});
return;
}

const actor = await apifyClient.actor(build.actId).get();

if (!actor) {
error({ message: `Actor with ID "${build.actId}" was not found.`, stdout: true });
return;
}

// Check if this tag already points to the same build
const existingTaggedBuilds = (actor.taggedBuilds ?? {}) as Record<string, ActorTaggedBuild>;
const existingTagData = existingTaggedBuilds[tag];

if (existingTagData?.buildId === buildId) {
warning({
message: `Build "${buildId}" is already tagged as "${tag}".`,
stdout: true,
});
return;
}

try {
// Update only the specific tag
await apifyClient.actor(build.actId).update({
taggedBuilds: {
[tag]: {
buildId: build.id,
},
},
} as never);

const previousBuildInfo = existingTagData?.buildNumber
? ` (previously pointed to build ${chalk.gray(existingTagData.buildNumber)})`
: '';

success({
message: `Tag "${chalk.yellow(tag)}" added to build ${chalk.gray(build.buildNumber)} (${chalk.gray(buildId)})${previousBuildInfo}`,
stdout: true,
});
} catch (err) {
const casted = err as ApifyApiError;
error({
message: `Failed to add tag "${tag}" to build "${buildId}".\n ${casted.message || casted}`,
stdout: true,
});
}
}
}
107 changes: 107 additions & 0 deletions src/commands/builds/remove-tag.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import type { ActorTaggedBuild, ApifyApiError } from 'apify-client';
import chalk from 'chalk';

import { ApifyCommand } from '../../lib/command-framework/apify-command.js';
import { Flags } from '../../lib/command-framework/flags.js';
import { useYesNoConfirm } from '../../lib/hooks/user-confirmations/useYesNoConfirm.js';
import { error, info, success } from '../../lib/outputs.js';
import { getLoggedClientOrThrow } from '../../lib/utils.js';

export class BuildsRemoveTagCommand extends ApifyCommand<typeof BuildsRemoveTagCommand> {
static override name = 'remove-tag' as const;

static override description = 'Removes a tag from a specific Actor build.';

static override flags = {
build: Flags.string({
char: 'b',
description: 'The build ID to remove the tag from.',
required: true,
}),
tag: Flags.string({
char: 't',
description: 'The tag to remove from the build.',
required: true,
}),
yes: Flags.boolean({
char: 'y',
description: 'Automatic yes to prompts; assume "yes" as answer to all prompts.',
default: false,
}),
};

async run() {
const { build: buildId, tag, yes } = this.flags;

const apifyClient = await getLoggedClientOrThrow();

const build = await apifyClient.build(buildId).get();

if (!build) {
error({ message: `Build with ID "${buildId}" was not found on your account.`, stdout: true });
return;
}

const actor = await apifyClient.actor(build.actId).get();

if (!actor) {
error({ message: `Actor with ID "${build.actId}" was not found.`, stdout: true });
return;
}

const existingTaggedBuilds = (actor.taggedBuilds ?? {}) as Record<string, ActorTaggedBuild>;
const existingTagData = existingTaggedBuilds[tag];

// Check if the tag exists
if (!existingTagData) {
error({
message: `Tag "${tag}" does not exist on Actor "${actor.name}".`,
stdout: true,
});
return;
}

// Check if the tag points to this build
if (existingTagData.buildId !== buildId) {
error({
message: `Tag "${tag}" is not associated with build "${buildId}". It points to build "${existingTagData.buildNumber}" (${existingTagData.buildId}).`,
stdout: true,
});
return;
}

// Confirm removal
const confirmed = await useYesNoConfirm({
message: `Are you sure you want to remove tag "${chalk.yellow(tag)}" from build ${chalk.gray(build.buildNumber)}?`,
providedConfirmFromStdin: yes || undefined,
});

if (!confirmed) {
info({
message: `Tag removal was canceled.`,
stdout: true,
});
return;
}

try {
// To remove a tag, set it to null
await apifyClient.actor(build.actId).update({
taggedBuilds: {
[tag]: null,
},
} as never);

success({
message: `Tag "${chalk.yellow(tag)}" removed from build ${chalk.gray(build.buildNumber)} (${chalk.gray(buildId)})`,
stdout: true,
});
} catch (err) {
const casted = err as ApifyApiError;
error({
message: `Failed to remove tag "${tag}" from build "${buildId}".\n ${casted.message || casted}`,
stdout: true,
});
}
}
}
Loading