Skip to content

Commit 082f981

Browse files
l2yshovladfrangu
andauthored
feat(builds): adds build tag commands (#1004)
Adds two new CLI commands for managing Actor build tags: #### New Commands **`apify builds add-tag`** - 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 remove-tag`** - 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. ``` #### Features - Validates build exists and has `SUCCEEDED` status before tagging - Shows informative message when reassigning an existing tag to a different build - Confirmation prompt before removing a tag (can be skipped with `--yes` flag) - Clear error messages for invalid operations #### Example Usage ```bash # Add a "beta" tag to a build apify builds add-tag -b abc123 -t beta # Remove a tag from a build apify builds remove-tag -b abc123 -t beta --yes ``` ### Changes - `src/commands/builds/add-tag.ts` - New command - `src/commands/builds/remove-tag.ts` - New command - `src/commands/builds/_index.ts` - Register new subcommands - `test/api/commands/builds/tags.test.ts` - API tests Closes #997 --------- Co-authored-by: Vlad Frangu <me@vladfrangu.dev>
1 parent ed91617 commit 082f981

8 files changed

Lines changed: 467 additions & 9 deletions

File tree

docs/reference.md

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -753,12 +753,28 @@ DESCRIPTION
753753
Manages Actor build processes and versioning.
754754
755755
SUBCOMMANDS
756-
builds rm Permanently removes an Actor build from the Apify
757-
platform.
758-
builds ls Lists all builds of the Actor.
759-
builds log Prints the log of a specific build.
760-
builds info Prints information about a specific build.
761-
builds create Creates a new build of the Actor.
756+
builds add-tag Adds a tag to a specific Actor build.
757+
builds remove-tag Removes a tag from a specific Actor build.
758+
builds rm Permanently removes an Actor build from
759+
the Apify platform.
760+
builds ls Lists all builds of the Actor.
761+
builds log Prints the log of a specific build.
762+
builds info Prints information about a specific build.
763+
builds create Creates a new build of the Actor.
764+
```
765+
766+
##### `apify builds add-tag`
767+
768+
```sh
769+
DESCRIPTION
770+
Adds a tag to a specific Actor build.
771+
772+
USAGE
773+
$ apify builds add-tag -b <value> -t <value>
774+
775+
FLAGS
776+
-b, --build=<value> The build ID to tag.
777+
-t, --tag=<value> The tag to add to the build.
762778
```
763779
764780
##### `apify builds create` / `apify actors build`
@@ -837,6 +853,22 @@ FLAGS
837853
--offset=<value> Number of builds that will be skipped.
838854
```
839855
856+
##### `apify builds remove-tag`
857+
858+
```sh
859+
DESCRIPTION
860+
Removes a tag from a specific Actor build.
861+
862+
USAGE
863+
$ apify builds remove-tag -b <value> -t <value> [-y]
864+
865+
FLAGS
866+
-b, --build=<value> The build ID to remove the tag from.
867+
-t, --tag=<value> The tag to remove from the build.
868+
-y, --yes Automatic yes to prompts; assume "yes"
869+
as answer to all prompts.
870+
```
871+
840872
##### `apify builds rm`
841873
842874
```sh

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@
8080
"@skyra/jaro-winkler": "^1.1.1",
8181
"adm-zip": "~0.5.15",
8282
"ajv": "~8.17.1",
83-
"apify-client": "^2.14.0",
83+
"apify-client": "~2.22.0",
8484
"archiver": "~7.0.1",
8585
"axios": "^1.11.0",
8686
"chalk": "~5.6.0",

scripts/generate-cli-docs.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,12 @@ const categories: Record<string, CommandsInCategory[]> = {
5151
'actor-build': [
5252
//
5353
{ command: Commands.builds },
54+
{ command: Commands.buildsAddTag },
5455
{ command: Commands.buildsCreate, aliases: [Commands.actorsBuild] },
5556
{ command: Commands.buildsInfo },
5657
{ command: Commands.buildsLog },
5758
{ command: Commands.buildsLs },
59+
{ command: Commands.buildsRemoveTag },
5860
{ command: Commands.buildsRm },
5961
],
6062
'actor-run': [

src/commands/builds/_index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { ApifyCommand } from '../../lib/command-framework/apify-command.js';
2+
import { BuildsAddTagCommand } from './add-tag.js';
23
import { BuildsCreateCommand } from './create.js';
34
import { BuildsInfoCommand } from './info.js';
45
import { BuildsLogCommand } from './log.js';
56
import { BuildsLsCommand } from './ls.js';
7+
import { BuildsRemoveTagCommand } from './remove-tag.js';
68
import { BuildsRmCommand } from './rm.js';
79

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

1315
static override subcommands = [
1416
//
17+
BuildsAddTagCommand,
18+
BuildsRemoveTagCommand,
1519
BuildsRmCommand,
1620
BuildsLsCommand,
1721
BuildsLogCommand,

src/commands/builds/add-tag.ts

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import type { ActorTaggedBuild, ApifyApiError } from 'apify-client';
2+
import chalk from 'chalk';
3+
4+
import { ApifyCommand } from '../../lib/command-framework/apify-command.js';
5+
import { Flags } from '../../lib/command-framework/flags.js';
6+
import { error, success, warning } from '../../lib/outputs.js';
7+
import { getLoggedClientOrThrow } from '../../lib/utils.js';
8+
9+
export class BuildsAddTagCommand extends ApifyCommand<typeof BuildsAddTagCommand> {
10+
static override name = 'add-tag' as const;
11+
12+
static override description = 'Adds a tag to a specific Actor build.';
13+
14+
static override flags = {
15+
build: Flags.string({
16+
char: 'b',
17+
description: 'The build ID to tag.',
18+
required: true,
19+
}),
20+
tag: Flags.string({
21+
char: 't',
22+
description: 'The tag to add to the build.',
23+
required: true,
24+
}),
25+
};
26+
27+
async run() {
28+
const { build: buildId, tag } = this.flags;
29+
30+
const apifyClient = await getLoggedClientOrThrow();
31+
32+
const build = await apifyClient.build(buildId).get();
33+
34+
if (!build) {
35+
error({ message: `Build with ID "${buildId}" was not found on your account.`, stdout: true });
36+
return;
37+
}
38+
39+
if (build.status !== 'SUCCEEDED') {
40+
error({
41+
message: `Build with ID "${buildId}" has status "${build.status}". Only successful builds can be tagged.`,
42+
stdout: true,
43+
});
44+
return;
45+
}
46+
47+
const actor = await apifyClient.actor(build.actId).get();
48+
49+
if (!actor) {
50+
error({ message: `Actor with ID "${build.actId}" was not found.`, stdout: true });
51+
return;
52+
}
53+
54+
// Check if this tag already points to the same build
55+
const existingTaggedBuilds = (actor.taggedBuilds ?? {}) as Record<string, ActorTaggedBuild>;
56+
const existingTagData = existingTaggedBuilds[tag];
57+
58+
if (existingTagData?.buildId === buildId) {
59+
warning({
60+
message: `Build "${buildId}" is already tagged as "${tag}".`,
61+
stdout: true,
62+
});
63+
return;
64+
}
65+
66+
try {
67+
// Update only the specific tag
68+
await apifyClient.actor(build.actId).update({
69+
taggedBuilds: {
70+
[tag]: {
71+
buildId: build.id,
72+
},
73+
},
74+
} as never);
75+
76+
const previousBuildInfo = existingTagData?.buildNumber
77+
? ` (previously pointed to build ${chalk.gray(existingTagData.buildNumber)})`
78+
: '';
79+
80+
success({
81+
message: `Tag "${chalk.yellow(tag)}" added to build ${chalk.gray(build.buildNumber)} (${chalk.gray(buildId)})${previousBuildInfo}`,
82+
stdout: true,
83+
});
84+
} catch (err) {
85+
const casted = err as ApifyApiError;
86+
error({
87+
message: `Failed to add tag "${tag}" to build "${buildId}".\n ${casted.message || casted}`,
88+
stdout: true,
89+
});
90+
}
91+
}
92+
}

src/commands/builds/remove-tag.ts

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import type { ActorTaggedBuild, ApifyApiError } from 'apify-client';
2+
import chalk from 'chalk';
3+
4+
import { ApifyCommand } from '../../lib/command-framework/apify-command.js';
5+
import { Flags } from '../../lib/command-framework/flags.js';
6+
import { useYesNoConfirm } from '../../lib/hooks/user-confirmations/useYesNoConfirm.js';
7+
import { error, info, success } from '../../lib/outputs.js';
8+
import { getLoggedClientOrThrow } from '../../lib/utils.js';
9+
10+
export class BuildsRemoveTagCommand extends ApifyCommand<typeof BuildsRemoveTagCommand> {
11+
static override name = 'remove-tag' as const;
12+
13+
static override description = 'Removes a tag from a specific Actor build.';
14+
15+
static override flags = {
16+
build: Flags.string({
17+
char: 'b',
18+
description: 'The build ID to remove the tag from.',
19+
required: true,
20+
}),
21+
tag: Flags.string({
22+
char: 't',
23+
description: 'The tag to remove from the build.',
24+
required: true,
25+
}),
26+
yes: Flags.boolean({
27+
char: 'y',
28+
description: 'Automatic yes to prompts; assume "yes" as answer to all prompts.',
29+
default: false,
30+
}),
31+
};
32+
33+
async run() {
34+
const { build: buildId, tag, yes } = this.flags;
35+
36+
const apifyClient = await getLoggedClientOrThrow();
37+
38+
const build = await apifyClient.build(buildId).get();
39+
40+
if (!build) {
41+
error({ message: `Build with ID "${buildId}" was not found on your account.`, stdout: true });
42+
return;
43+
}
44+
45+
const actor = await apifyClient.actor(build.actId).get();
46+
47+
if (!actor) {
48+
error({ message: `Actor with ID "${build.actId}" was not found.`, stdout: true });
49+
return;
50+
}
51+
52+
const existingTaggedBuilds = (actor.taggedBuilds ?? {}) as Record<string, ActorTaggedBuild>;
53+
const existingTagData = existingTaggedBuilds[tag];
54+
55+
// Check if the tag exists
56+
if (!existingTagData) {
57+
error({
58+
message: `Tag "${tag}" does not exist on Actor "${actor.name}".`,
59+
stdout: true,
60+
});
61+
return;
62+
}
63+
64+
// Check if the tag points to this build
65+
if (existingTagData.buildId !== buildId) {
66+
error({
67+
message: `Tag "${tag}" is not associated with build "${buildId}". It points to build "${existingTagData.buildNumber}" (${existingTagData.buildId}).`,
68+
stdout: true,
69+
});
70+
return;
71+
}
72+
73+
// Confirm removal
74+
const confirmed = await useYesNoConfirm({
75+
message: `Are you sure you want to remove tag "${chalk.yellow(tag)}" from build ${chalk.gray(build.buildNumber)}?`,
76+
providedConfirmFromStdin: yes || undefined,
77+
});
78+
79+
if (!confirmed) {
80+
info({
81+
message: `Tag removal was canceled.`,
82+
stdout: true,
83+
});
84+
return;
85+
}
86+
87+
try {
88+
// To remove a tag, set it to null
89+
await apifyClient.actor(build.actId).update({
90+
taggedBuilds: {
91+
[tag]: null,
92+
},
93+
} as never);
94+
95+
success({
96+
message: `Tag "${chalk.yellow(tag)}" removed from build ${chalk.gray(build.buildNumber)} (${chalk.gray(buildId)})`,
97+
stdout: true,
98+
});
99+
} catch (err) {
100+
const casted = err as ApifyApiError;
101+
error({
102+
message: `Failed to remove tag "${tag}" from build "${buildId}".\n ${casted.message || casted}`,
103+
stdout: true,
104+
});
105+
}
106+
}
107+
}

0 commit comments

Comments
 (0)