Skip to content

Commit 804d050

Browse files
authored
Feat: deployctl deployments delete (#278)
1 parent cfd9a67 commit 804d050

File tree

2 files changed

+168
-93
lines changed

2 files changed

+168
-93
lines changed

src/subcommands/deployments.ts

Lines changed: 152 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -78,23 +78,24 @@ SUBCOMMANDS:
7878
show the details of the current production deployment of the project specified in the config file or with the --project option.
7979
Use --next and --prev to fetch the deployments deployed after or before the specified (or production) deployment.
8080
list List the deployments of a project. Specify the project using --project. Pagination can be controlled with --page and --limit.
81+
delete Delete a deployment. Same options to select the deployment as the show subcommand apply (--id, --project, --next and --prev).
8182
8283
OPTIONS:
8384
-h, --help Prints this help information
84-
--id=<deployment-id> [show] Id of the deployment of which to show details
85-
-p, --project=<NAME|ID> [show] The project the production deployment of which to show the details. Ignored if combined with --id
85+
--id=<deployment-id> [show,delete] Select a deployment by id.
86+
-p, --project=<NAME|ID> [show,delete] Select the production deployment of a project. Ignored if combined with --id
8687
[list] The project of which to list deployments.
87-
--next[=pos] [show] Show the details of a deployment deployed after the specified deployment
88+
--next[=pos] [show,delete] Modifier that selects a deployment deployed chronologically after the deployment selected with --id or --project
8889
Can be used multiple times (--next --next is the same as --next=2)
89-
--prev[=pos] [show] Show the details of a deployment deployed before the specified deployment.
90+
--prev[=pos] [show,delete] Modifier that selects a deployment deployed chronologically before the deployment selected with --id or --project
9091
Can be used multiple times (--prev --prev is the same as --prev=2)
9192
--page=<num> [list] Page of the deployments list to fetch
9293
--limit=<num> [list] Amount of deployments to include in the list
9394
--format=<overview|json> Output the deployment details in an overview or JSON-encoded. Defaults to 'overview' when stdout is a tty, and 'json' otherwise.
9495
--token=<TOKEN> The API token to use (defaults to DENO_DEPLOY_TOKEN env var)
9596
--config=<PATH> Path to the file from where to load DeployCTL config. Defaults to 'deno.json'
9697
--color=<auto|always|never> Enable or disable colored output. Defaults to 'auto' (colored when stdout is a tty)
97-
--force Automatically execute the command without waiting for confirmation.
98+
--force [delete] Automatically execute the command without waiting for confirmation.
9899
`;
99100

100101
export default async function (args: Args): Promise<void> {
@@ -110,6 +111,9 @@ export default async function (args: Args): Promise<void> {
110111
case "show":
111112
await showDeployment(args);
112113
break;
114+
case "delete":
115+
await deleteDeployment(args);
116+
break;
113117
default:
114118
console.error(help);
115119
Deno.exit(1);
@@ -212,98 +216,20 @@ async function listDeployments(args: Args): Promise<void> {
212216

213217
// TODO: Show if active (and maybe some stats?)
214218
async function showDeployment(args: Args): Promise<void> {
215-
const deploymentIdArg = args._.shift()?.toString() || args.id;
216-
// Ignore --project if user also provided --id
217-
const projectIdArg = deploymentIdArg ? undefined : args.project;
218-
219219
const api = args.token
220220
? API.fromToken(args.token)
221221
: API.withTokenProvisioner(TokenProvisioner);
222222

223-
let deploymentId,
224-
projectId,
225-
build: Build | null | undefined,
226-
project: Project | null | undefined,
227-
databases: Database[] | null;
228-
229-
if (deploymentIdArg) {
230-
deploymentId = deploymentIdArg;
231-
} else {
232-
// Default to showing the production deployment of the project
233-
if (!projectIdArg) {
234-
error(
235-
"No deployment or project specified. Use --id <deployment-id> or --project <project-name>",
236-
);
237-
}
238-
projectId = projectIdArg;
239-
const spinner = wait(
240-
`Searching production deployment of project '${projectId}'...`,
241-
).start();
242-
project = await api.getProject(projectId);
243-
if (!project) {
244-
spinner.fail(
245-
`The project '${projectId}' does not exist, or you don't have access to it`,
246-
);
247-
return Deno.exit(1);
248-
}
249-
if (!project.productionDeployment) {
250-
spinner.fail(
251-
`Project '${project.name}' does not have a production deployment. Use --id <deployment-id> to specify the deployment to show`,
252-
);
253-
return Deno.exit(1);
254-
}
255-
deploymentId = project.productionDeployment.deploymentId;
256-
spinner.succeed(
257-
`The production deployment of the project '${project.name}' is '${deploymentId}'`,
258-
);
259-
}
260-
261-
if (args.prev.length !== 0 || args.next.length !== 0) {
262-
// Search the deployment relative to the specified deployment
263-
if (!projectId) {
264-
// Fetch the deployment specified with --id, to know of which project to search the relative deployment
265-
// If user didn't use --id, they must have used --project, thus we already know the project-id
266-
const spinner_ = wait(`Fetching deployment '${deploymentId}'...`)
267-
.start();
268-
const specifiedDeployment = await api.getDeployment(deploymentId);
269-
if (!specifiedDeployment) {
270-
spinner_.fail(
271-
`The deployment '${deploymentId}' does not exist, or you don't have access to it`,
272-
);
273-
return Deno.exit(1);
274-
}
275-
spinner_.succeed(`Deployment '${deploymentId}' found`);
276-
projectId = specifiedDeployment.project.id;
277-
}
278-
let relativePos = 0;
279-
for (const prev of args.prev) {
280-
relativePos -= parseInt(prev || "1");
281-
}
282-
for (const next of args.next) {
283-
relativePos += parseInt(next || "1");
284-
}
285-
const relativePosString = relativePos.toLocaleString(navigator.language, {
286-
signDisplay: "exceptZero",
287-
});
288-
const spinner = wait(
289-
`Searching the deployment ${relativePosString} relative to '${deploymentId}'...`,
290-
).start();
291-
const maybeBuild = await searchRelativeDeployment(
292-
api.listAllDeployments(projectId),
293-
deploymentId,
294-
relativePos,
295-
);
296-
if (!maybeBuild) {
297-
spinner.fail(
298-
`The deployment '${deploymentId}' does not have a deployment ${relativePosString} relative to it`,
299-
);
300-
return Deno.exit(1);
301-
}
302-
build = maybeBuild;
303-
spinner.succeed(
304-
`The deployment ${relativePosString} relative to '${deploymentId}' is '${build.deploymentId}'`,
305-
);
306-
}
223+
let [deploymentId, projectId, build, project]: [
224+
string,
225+
string | undefined,
226+
Build | null | undefined,
227+
Project | null | undefined,
228+
] = await resolveDeploymentId(
229+
args,
230+
api,
231+
);
232+
let databases: Database[] | null;
307233

308234
const spinner = wait(`Fetching deployment '${deploymentId}' details...`)
309235
.start();
@@ -375,6 +301,35 @@ async function showDeployment(args: Args): Promise<void> {
375301
}
376302
}
377303

304+
async function deleteDeployment(args: Args): Promise<void> {
305+
const api = args.token
306+
? API.fromToken(args.token)
307+
: API.withTokenProvisioner(TokenProvisioner);
308+
const [deploymentId, _projectId, _build, _project] =
309+
await resolveDeploymentId(
310+
args,
311+
api,
312+
);
313+
const confirmation = args.force ? true : confirm(
314+
`${
315+
magenta("?")
316+
} Are you sure you want to delete the deployment '${deploymentId}'?`,
317+
);
318+
if (!confirmation) {
319+
wait("").fail("Delete canceled");
320+
return;
321+
}
322+
const spinner = wait(`Deleting deployment '${deploymentId}'...`).start();
323+
const deleted = await api.deleteDeployment(deploymentId);
324+
if (deleted) {
325+
spinner.succeed(`Deployment '${deploymentId}' deleted successfully`);
326+
} else {
327+
spinner.fail(
328+
`Deployment '${deploymentId}' not found, or you don't have access to it`,
329+
);
330+
}
331+
}
332+
378333
async function searchRelativeDeployment(
379334
deployments: AsyncGenerator<Build>,
380335
deploymentId: string,
@@ -722,3 +677,107 @@ function renderTable(table: Record<string, string>[]) {
722677
}
723678
console.log(`\u2514\u2500${divisor}\u2500\u2518`);
724679
}
680+
681+
type DeploymentId = string;
682+
type ProjectId = string;
683+
684+
async function resolveDeploymentId(
685+
args: Args,
686+
api: API,
687+
): Promise<
688+
[DeploymentId, ProjectId | undefined, Build | undefined, Project | undefined]
689+
> {
690+
const deploymentIdArg = args._.shift()?.toString() || args.id;
691+
// Ignore --project if user also provided --id
692+
const projectIdArg = deploymentIdArg ? undefined : args.project;
693+
694+
let deploymentId,
695+
projectId: string | undefined,
696+
build: Build | undefined,
697+
project: Project | undefined;
698+
699+
if (deploymentIdArg) {
700+
deploymentId = deploymentIdArg;
701+
} else {
702+
// Default to showing the production deployment of the project
703+
if (!projectIdArg) {
704+
error(
705+
"No deployment or project specified. Use --id <deployment-id> or --project <project-name>",
706+
);
707+
}
708+
projectId = projectIdArg;
709+
const spinner = wait(
710+
`Searching production deployment of project '${projectId}'...`,
711+
).start();
712+
const maybeProject = await api.getProject(projectId);
713+
if (!maybeProject) {
714+
spinner.fail(
715+
`The project '${projectId}' does not exist, or you don't have access to it`,
716+
);
717+
return Deno.exit(1);
718+
}
719+
project = maybeProject;
720+
if (!project.productionDeployment) {
721+
spinner.fail(
722+
`Project '${project.name}' does not have a production deployment. Use --id <deployment-id> to specify the deployment to show`,
723+
);
724+
return Deno.exit(1);
725+
}
726+
deploymentId = project.productionDeployment.deploymentId;
727+
spinner.succeed(
728+
`The production deployment of the project '${project.name}' is '${deploymentId}'`,
729+
);
730+
}
731+
732+
if (args.prev.length !== 0 || args.next.length !== 0) {
733+
// Search the deployment relative to the specified deployment
734+
if (!projectId) {
735+
// Fetch the deployment specified with --id, to know of which project to search the relative deployment
736+
// If user didn't use --id, they must have used --project, thus we already know the project-id
737+
const spinner_ = wait(`Fetching deployment '${deploymentId}'...`)
738+
.start();
739+
const specifiedDeployment = await api.getDeployment(deploymentId);
740+
if (!specifiedDeployment) {
741+
spinner_.fail(
742+
`The deployment '${deploymentId}' does not exist, or you don't have access to it`,
743+
);
744+
return Deno.exit(1);
745+
}
746+
spinner_.succeed(`Deployment '${deploymentId}' found`);
747+
projectId = specifiedDeployment.project.id;
748+
}
749+
let relativePos = 0;
750+
for (const prev of args.prev) {
751+
relativePos -= parseInt(prev || "1");
752+
}
753+
for (const next of args.next) {
754+
relativePos += parseInt(next || "1");
755+
}
756+
if (Number.isNaN(relativePos)) {
757+
error("Value of --next and --prev must be a number");
758+
}
759+
const relativePosString = relativePos.toLocaleString(navigator.language, {
760+
signDisplay: "exceptZero",
761+
});
762+
const spinner = wait(
763+
`Searching the deployment ${relativePosString} relative to '${deploymentId}'...`,
764+
).start();
765+
const maybeBuild = await searchRelativeDeployment(
766+
api.listAllDeployments(projectId),
767+
deploymentId,
768+
relativePos,
769+
);
770+
if (!maybeBuild) {
771+
spinner.fail(
772+
`The deployment '${deploymentId}' does not have a deployment ${relativePosString} relative to it`,
773+
);
774+
return Deno.exit(1);
775+
}
776+
build = maybeBuild;
777+
deploymentId = build.deploymentId;
778+
spinner.succeed(
779+
`The deployment ${relativePosString} relative to '${deploymentId}' is '${build.deploymentId}'`,
780+
);
781+
}
782+
return [deploymentId, projectId, build, project];
783+
}

src/utils/api.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,22 @@ export class API {
312312
}
313313
}
314314

315+
async deleteDeployment(
316+
deploymentId: string,
317+
): Promise<boolean> {
318+
try {
319+
await this.#requestJson(`/v1/deployments/${deploymentId}`, {
320+
method: "DELETE",
321+
});
322+
return true;
323+
} catch (err) {
324+
if (err instanceof APIError && err.code === "deploymentNotFound") {
325+
return false;
326+
}
327+
throw err;
328+
}
329+
}
330+
315331
getLogs(
316332
projectId: string,
317333
deploymentId: string,

0 commit comments

Comments
 (0)