1import * as ecs from "@distilled.cloud/aws/ecs";
2import * as Effect from "effect/Effect";
3
4/**
5 * Fully reclaim every revision of an out-of-band task definition family:
6 * deregister any ACTIVE revisions, then hard-delete the INACTIVE revisions
7 * via `deleteTaskDefinitions` (deregistering alone leaves them INACTIVE,
8 * which still shows up in a live `listTaskDefinitions --status INACTIVE`
9 * sweep).
10 *
11 * Idempotent — run it BEFORE a test to recover leftovers from a previously
12 * killed run (the family names are deterministic, so a re-run reclaims a
13 * prior orphan instead of stacking new revisions forever), and as a
14 * finalizer AFTER the test so a passing test leaves zero task definitions
15 * behind.
16 */
17export const reclaimTaskDefinitionFamily = (family: string) =>
18 Effect.gen(function* () {
19 // `familyPrefix` is a prefix match — filter to the exact family so a
20 // family that happens to prefix another test's family is untouched.
21 const list = (status: "ACTIVE" | "INACTIVE") =>
22 ecs
23 .listTaskDefinitions({ familyPrefix: family, status })
24 .pipe(
25 Effect.map((r) =>
26 (r.taskDefinitionArns ?? []).filter((arn) =>
27 arn.includes(`/${family}:`),
28 ),
29 ),
30 );
31
32 const active = yield* list("ACTIVE");
33 yield* Effect.forEach(active, (arn) =>
34 ecs
35 .deregisterTaskDefinition({ taskDefinition: arn })
36 .pipe(Effect.catchTag("ClientException", () => Effect.void)),
37 );
38
39 const inactive = yield* list("INACTIVE");
40 // deleteTaskDefinitions accepts at most 10 ARNs per call.
41 for (let i = 0; i < inactive.length; i += 10) {
42 yield* ecs
43 .deleteTaskDefinitions({ taskDefinitions: inactive.slice(i, i + 10) })
44 .pipe(Effect.catchTag("ClientException", () => Effect.void));
45 }
46 });