Skip to content

Commit a640c5b

Browse files
committed
fix(extensions): pull the service image on install/update so moving tags refresh
An extension's service can pin a moving image tag (e.g. :latest) and never touch it per release; the platform refreshes it. dockerComposeAction gains a `pull` action and the installer runs `docker compose pull <svc>` before `up -d` — `up` alone won't re-pull a tag already cached on the host, so an updated extension would otherwise keep running the old image. Best-effort: a pull failure falls through to the cached image.
1 parent 1a6bc7e commit a640c5b

2 files changed

Lines changed: 28 additions & 12 deletions

File tree

apps/api/src/services/extension-installer.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,18 @@ export async function installExtension(
154154
await ctx.log("Rendering compose...");
155155
await renderAndPersistCompose();
156156
for (const svc of serviceComponents) {
157+
// Pull first so a moving image tag (e.g. :latest) is refreshed on
158+
// update — `up -d` alone won't re-pull a tag already cached on the host,
159+
// so without this an updated extension would keep running the old image.
160+
// Best-effort: a pull failure (offline / locally-built image) falls
161+
// through to starting on the cached image.
162+
const pulled = await dockerComposeAction(svc.service, "pull");
163+
if (pulled.exitCode !== 0) {
164+
await ctx.log(
165+
`docker compose pull ${svc.service} failed (exit ${pulled.exitCode}); starting on the cached image`,
166+
"stderr",
167+
);
168+
}
157169
await serviceStart(svc.service, ctx);
158170
}
159171
}

apps/api/src/utils/docker-compose.ts

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -34,23 +34,27 @@ export async function dockerComposePs(): Promise<PsEntry[]> {
3434

3535
export async function dockerComposeAction(
3636
serviceId: string,
37-
action: "start" | "stop" | "restart" | "recreate" | "remove",
37+
action: "start" | "stop" | "restart" | "recreate" | "remove" | "pull",
3838
): Promise<{ exitCode: number; stdout: string }> {
3939
const serviceArgs = serviceId ? [serviceId] : [];
4040
const args =
4141
action === "start"
4242
? ["up", "-d", ...serviceArgs]
43-
: action === "recreate"
44-
? // Force-recreate so a rotated secret (same key/path, new file content)
45-
// is always picked up, not just config-shape changes.
46-
["up", "-d", "--force-recreate", ...serviceArgs]
47-
: action === "stop"
48-
? ["stop", ...serviceArgs]
49-
: action === "remove"
50-
? // Stop + remove the container (clean teardown when uninstalling an
51-
// extension's service). `-s` stops first, `-f` skips confirmation.
52-
["rm", "-sf", ...serviceArgs]
53-
: ["restart", ...serviceArgs];
43+
: action === "pull"
44+
? // Re-fetch the image so a moving tag (e.g. :latest) is refreshed; a
45+
// following `up -d` then recreates the container on the new digest.
46+
["pull", ...serviceArgs]
47+
: action === "recreate"
48+
? // Force-recreate so a rotated secret (same key/path, new file content)
49+
// is always picked up, not just config-shape changes.
50+
["up", "-d", "--force-recreate", ...serviceArgs]
51+
: action === "stop"
52+
? ["stop", ...serviceArgs]
53+
: action === "remove"
54+
? // Stop + remove the container (clean teardown when uninstalling an
55+
// extension's service). `-s` stops first, `-f` skips confirmation.
56+
["rm", "-sf", ...serviceArgs]
57+
: ["restart", ...serviceArgs];
5458
try {
5559
const { stdout } = await execFile("docker", ["compose", "-f", composePath(), ...args], {
5660
timeout: 120_000,

0 commit comments

Comments
 (0)