From 4c49fa334e447a13e4ede4b90638e2b9c5fffdbe Mon Sep 17 00:00:00 2001 From: Jason Penilla <11360596+jpenilla@users.noreply.github.com> Date: Mon, 14 Sep 2026 16:20:11 -0700 Subject: [PATCH 1/6] feat(tool): install nub from GitHub release binaries --- src/cli/install-tool/index.ts | 2 ++ src/cli/tools/index.ts | 1 + src/cli/tools/nub.ts | 53 +++++++++++++++++++++++++++++++++++ test/latest/Dockerfile | 3 ++ test/latest/Dockerfile.arm64 | 9 ++++++ 5 files changed, 68 insertions(+) create mode 100644 src/cli/tools/nub.ts diff --git a/src/cli/install-tool/index.ts b/src/cli/install-tool/index.ts index 6f9860e93f..c7bd16d531 100644 --- a/src/cli/install-tool/index.ts +++ b/src/cli/install-tool/index.ts @@ -81,6 +81,7 @@ import { YarnVersionResolver, } from '../tools/node/resolver.ts'; import { NpmBaseInstallService } from '../tools/node/utils.ts'; +import { NubInstallService } from '../tools/nub.ts'; import { ComposerInstallService, ComposerVersionResolver, @@ -177,6 +178,7 @@ async function prepareInstallContainer(): Promise { container.bind(INSTALL_TOOL_TOKEN).to(NixInstallService); container.bind(INSTALL_TOOL_TOKEN).to(NugetInstallService); container.bind(INSTALL_TOOL_TOKEN).to(NodeInstallService); + container.bind(INSTALL_TOOL_TOKEN).to(NubInstallService); container.bind(INSTALL_TOOL_TOKEN).to(PaketInstallService); container.bind(INSTALL_TOOL_TOKEN).to(PhpInstallService); container.bind(INSTALL_TOOL_TOKEN).to(PixiInstallService); diff --git a/src/cli/tools/index.ts b/src/cli/tools/index.ts index 7e0b817fc6..113291a609 100644 --- a/src/cli/tools/index.ts +++ b/src/cli/tools/index.ts @@ -33,6 +33,7 @@ export const NoPrepareTools = [ 'maven', 'mise', 'nix', + 'nub', 'nuget', 'npm', 'paket', diff --git a/src/cli/tools/nub.ts b/src/cli/tools/nub.ts new file mode 100644 index 0000000000..e8885b1394 --- /dev/null +++ b/src/cli/tools/nub.ts @@ -0,0 +1,53 @@ +import fs from 'node:fs/promises'; +import { join } from 'node:path'; +import { injectFromHierarchy, injectable } from 'inversify'; +import { BaseInstallService } from '../install-tool/base-install.service.ts'; + +@injectable() +@injectFromHierarchy() +export class NubInstallService extends BaseInstallService { + readonly name = 'nub'; + + private get ghArch(): string { + switch (this.envSvc.arch) { + case 'arm64': + return 'arm64'; + case 'amd64': + return 'x64'; + } + } + + override async install(version: string): Promise { + const baseUrl = `https://github.com/nubjs/nub/releases/download/v${version}/`; + const filename = `nub-linux-${this.ghArch}.tar.gz`; + const url = `${baseUrl}${filename}`; + + const checksumFile = await this.http.download({ url: `${url}.sha256` }); + const expectedChecksum = (await fs.readFile(checksumFile, 'utf-8')) + .trim() + .split(/\s+/)[0]; + if (!expectedChecksum || !/^[a-f0-9]{64}$/i.test(expectedChecksum)) { + throw new Error(`Invalid checksum for ${filename}`); + } + + const file = await this.http.download({ + url, + checksumType: 'sha256', + expectedChecksum, + }); + + await this.pathSvc.ensureToolPath(this.name); + const path = await this.pathSvc.createVersionedToolPath(this.name, version); + // Preserve the release layout: bin/ and runtime/ are siblings. + await this.compress.extract({ file, cwd: path }); + } + + override async link(version: string): Promise { + const src = join(this.pathSvc.versionedToolPath(this.name, version), 'bin'); + await this.shellwrapper({ srcDir: src }); + } + + override async test(_version: string): Promise { + await this._spawn(this.name, ['--version']); + } +} diff --git a/test/latest/Dockerfile b/test/latest/Dockerfile index 0fc7315d8f..7824315923 100644 --- a/test/latest/Dockerfile +++ b/test/latest/Dockerfile @@ -225,6 +225,9 @@ RUN install-tool buf v1.73.0 # renovate: datasource=npm RUN install-tool bun 1.4.2 +# renovate: datasource=github-releases packageName=nubjs/nub +RUN install-tool nub 0.9.2 + # renovate: datasource=github-releases packageName=denoland/deno RUN install-tool deno 2.9.7 diff --git a/test/latest/Dockerfile.arm64 b/test/latest/Dockerfile.arm64 index 8e3e58149d..35118575ab 100644 --- a/test/latest/Dockerfile.arm64 +++ b/test/latest/Dockerfile.arm64 @@ -48,6 +48,14 @@ FROM base AS test-bun # renovate: datasource=npm RUN install-tool bun 1.4.2 +#-------------------------------------- +# Image: nub +#-------------------------------------- +FROM base AS test-nub + +# renovate: datasource=github-releases packageName=nubjs/nub +RUN install-tool nub 0.9.2 + #-------------------------------------- # Image: deno #-------------------------------------- @@ -220,6 +228,7 @@ FROM base COPY --from=test-bazelisk /.dummy /.dummy COPY --from=test-bun /.dummy /.dummy +COPY --from=test-nub /.dummy /.dummy COPY --from=test-deno /.dummy /.dummy COPY --from=test-apko /.dummy /.dummy COPY --from=test-apm /.dummy /.dummy From 6eba5ead6447b5e308abb2504df49abba483d3ae Mon Sep 17 00:00:00 2001 From: Jason Penilla <11360596+jpenilla@users.noreply.github.com> Date: Mon, 14 Sep 2026 16:34:34 -0700 Subject: [PATCH 2/6] docs: document nub release download URLs --- docs/custom-registries.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/docs/custom-registries.md b/docs/custom-registries.md index d915a42ad4..a6999ea227 100644 --- a/docs/custom-registries.md +++ b/docs/custom-registries.md @@ -611,6 +611,21 @@ https://github.com/containerbase/node-re2-prebuild/releases/download/1.20.9/linu https://github.com/containerbase/node-re2-prebuild/releases/download/1.20.9/linux-x64-108.br ``` +## `nub` + +Nub releases are downloaded from: + +- `https://github.com/nubjs/nub/releases` + +Samples: + +```txt +https://github.com/nubjs/nub/releases/download/v0.9.2/nub-linux-x64.tar.gz +https://github.com/nubjs/nub/releases/download/v0.9.2/nub-linux-x64.tar.gz.sha256 +https://github.com/nubjs/nub/releases/download/v0.9.2/nub-linux-arm64.tar.gz +https://github.com/nubjs/nub/releases/download/v0.9.2/nub-linux-arm64.tar.gz.sha256 +``` + ## `php` PHP releases are downloaded from: From a5aaa52bb3ecba3614d0ed30b2ff92cb63256069 Mon Sep 17 00:00:00 2001 From: Jason Penilla <11360596+jpenilla@users.noreply.github.com> Date: Wed, 23 Sep 2026 15:04:56 -0700 Subject: [PATCH 3/6] chore(nub): update release examples and tests to v0.9.3 --- docs/custom-registries.md | 8 ++++---- test/latest/Dockerfile | 2 +- test/latest/Dockerfile.arm64 | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/custom-registries.md b/docs/custom-registries.md index a6999ea227..54ffa19e53 100644 --- a/docs/custom-registries.md +++ b/docs/custom-registries.md @@ -620,10 +620,10 @@ Nub releases are downloaded from: Samples: ```txt -https://github.com/nubjs/nub/releases/download/v0.9.2/nub-linux-x64.tar.gz -https://github.com/nubjs/nub/releases/download/v0.9.2/nub-linux-x64.tar.gz.sha256 -https://github.com/nubjs/nub/releases/download/v0.9.2/nub-linux-arm64.tar.gz -https://github.com/nubjs/nub/releases/download/v0.9.2/nub-linux-arm64.tar.gz.sha256 +https://github.com/nubjs/nub/releases/download/v0.9.3/nub-linux-x64.tar.gz +https://github.com/nubjs/nub/releases/download/v0.9.3/nub-linux-x64.tar.gz.sha256 +https://github.com/nubjs/nub/releases/download/v0.9.3/nub-linux-arm64.tar.gz +https://github.com/nubjs/nub/releases/download/v0.9.3/nub-linux-arm64.tar.gz.sha256 ``` ## `php` diff --git a/test/latest/Dockerfile b/test/latest/Dockerfile index 7824315923..ee8902dbee 100644 --- a/test/latest/Dockerfile +++ b/test/latest/Dockerfile @@ -226,7 +226,7 @@ RUN install-tool buf v1.73.0 RUN install-tool bun 1.4.2 # renovate: datasource=github-releases packageName=nubjs/nub -RUN install-tool nub 0.9.2 +RUN install-tool nub 0.9.3 # renovate: datasource=github-releases packageName=denoland/deno RUN install-tool deno 2.9.7 diff --git a/test/latest/Dockerfile.arm64 b/test/latest/Dockerfile.arm64 index 35118575ab..1934b61245 100644 --- a/test/latest/Dockerfile.arm64 +++ b/test/latest/Dockerfile.arm64 @@ -54,7 +54,7 @@ RUN install-tool bun 1.4.2 FROM base AS test-nub # renovate: datasource=github-releases packageName=nubjs/nub -RUN install-tool nub 0.9.2 +RUN install-tool nub 0.9.3 #-------------------------------------- # Image: deno From 9994070d3eaeb62bfab7f522d10ed5649ec81ac1 Mon Sep 17 00:00:00 2001 From: Jason Penilla <11360596+jpenilla@users.noreply.github.com> Date: Wed, 23 Sep 2026 15:14:36 -0700 Subject: [PATCH 4/6] chore(nub): include tool in test metadata --- .github/renovate.json | 2 ++ test/latest/Dockerfile | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/renovate.json b/.github/renovate.json index 0b2362e818..7f1535b31b 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -91,6 +91,7 @@ "nix", "node", "npm", + "nub", "pdm", "pipenv", "pixi", @@ -146,6 +147,7 @@ "nix", "node", "npm", + "nub", "pdm", "pipenv", "pixi", diff --git a/test/latest/Dockerfile b/test/latest/Dockerfile index ee8902dbee..f127352ed1 100644 --- a/test/latest/Dockerfile +++ b/test/latest/Dockerfile @@ -212,7 +212,7 @@ RUN prepare-tool all RUN set -ex; [ -d /usr/local/erlang ] && echo "works" || exit 1; #-------------------------------------- -# test: apm, bazelisk, buf, bun, deno, devbox, gh, helmfile, kustomize, skopeo, tofu, vendir +# test: apm, bazelisk, buf, bun, deno, devbox, gh, helmfile, kustomize, nub, skopeo, tofu, vendir #-------------------------------------- FROM base AS teste From 2009b01cb7a877f17036c82bea321f8a9b48d187 Mon Sep 17 00:00:00 2001 From: Jason Penilla <11360596+jpenilla@users.noreply.github.com> Date: Wed, 23 Sep 2026 15:21:07 -0700 Subject: [PATCH 5/6] test(nub): cover release installation and checksum validation --- src/cli/tools/nub.spec.ts | 91 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 src/cli/tools/nub.spec.ts diff --git a/src/cli/tools/nub.spec.ts b/src/cli/tools/nub.spec.ts new file mode 100644 index 0000000000..34584c3e8b --- /dev/null +++ b/src/cli/tools/nub.spec.ts @@ -0,0 +1,91 @@ +import { arch } from 'node:os'; +import { join } from 'node:path'; +import { beforeAll, beforeEach, describe, expect, test, vi } from 'vitest'; +import { CompressionService, LinkToolService } from '../services/index.ts'; +import { NubInstallService } from './nub.ts'; +import { scope } from '~test/http-mock.ts'; +import { ensurePaths } from '~test/path.ts'; +import { checksum, toolContext } from '~test/tool.ts'; + +const { execaMock } = vi.hoisted(() => ({ execaMock: vi.fn() })); +vi.mock('execa', () => ({ execa: execaMock })); +vi.mock('node:os', async (importOriginal) => ({ + ...(await importOriginal()), + arch: vi.fn(() => 'x64'), +})); + +const baseUrl = 'https://github.com'; +const archive = 'nub archive'; + +describe('cli/tools/nub', () => { + beforeAll(async () => { + await ensurePaths(['tmp', 'opt/containerbase/bin']); + }); + + beforeEach(() => { + vi.mocked(arch).mockReturnValue('x64'); + execaMock.mockResolvedValue({ failed: false }); + }); + + test.each([ + { hostArch: 'x64', ghArch: 'x64', version: '0.9.3' }, + { hostArch: 'arm64', ghArch: 'arm64', version: '0.9.2' }, + ] as const)('install on $ghArch', async ({ hostArch, ghArch, version }) => { + vi.mocked(arch).mockReturnValue(hostArch); + const { svc, pathSvc } = await toolContext(NubInstallService); + const filename = `nub-linux-${ghArch}.tar.gz`; + const releaseUrl = `/nubjs/nub/releases/download/v${version}`; + scope(baseUrl) + .get(`${releaseUrl}/${filename}.sha256`) + .reply(200, `${checksum(archive)} ${filename}\n`) + .get(`${releaseUrl}/${filename}`) + .reply(200, archive); + const extract = vi.spyOn(CompressionService.prototype, 'extract'); + + await expect(svc.install(version)).resolves.toBeUndefined(); + + expect(extract).toHaveBeenCalledExactlyOnceWith({ + file: expect.stringContaining(filename), + cwd: pathSvc.versionedToolPath('nub', version), + }); + }); + + test.each([ + { version: '0.9.4', body: '' }, + { version: '0.9.5', body: 'invalid checksum' }, + ])('rejects invalid checksum $body', async ({ version, body }) => { + const { svc } = await toolContext(NubInstallService); + scope(baseUrl) + .get( + `/nubjs/nub/releases/download/v${version}/nub-linux-x64.tar.gz.sha256`, + ) + .reply(200, body); + + await expect(svc.install(version)).rejects.toThrow( + 'Invalid checksum for nub-linux-x64.tar.gz', + ); + }); + + test('link', async () => { + const { svc, pathSvc } = await toolContext(NubInstallService); + const spy = vi.spyOn(LinkToolService.prototype, 'shellwrapper'); + + await expect(svc.link('0.9.3')).resolves.toBeUndefined(); + + expect(spy).toHaveBeenCalledExactlyOnceWith('nub', { + srcDir: join(pathSvc.versionedToolPath('nub', '0.9.3'), 'bin'), + }); + }); + + test('runs the tool test', async () => { + const { svc } = await toolContext(NubInstallService); + + await expect(svc.test('0.9.3')).resolves.toBeUndefined(); + + expect(execaMock).toHaveBeenCalledWith( + 'nub', + ['--version'], + expect.any(Object), + ); + }); +}); From 237f07a45f8196580b40013819a26b71a1671d63 Mon Sep 17 00:00:00 2001 From: Jason Penilla <11360596+jpenilla@users.noreply.github.com> Date: Thu, 24 Sep 2026 07:34:50 -0700 Subject: [PATCH 6/6] refactor(nub): use shared checksum helper --- src/cli/tools/nub.spec.ts | 16 ---------------- src/cli/tools/nub.ts | 9 +-------- 2 files changed, 1 insertion(+), 24 deletions(-) diff --git a/src/cli/tools/nub.spec.ts b/src/cli/tools/nub.spec.ts index 34584c3e8b..313f3b4826 100644 --- a/src/cli/tools/nub.spec.ts +++ b/src/cli/tools/nub.spec.ts @@ -50,22 +50,6 @@ describe('cli/tools/nub', () => { }); }); - test.each([ - { version: '0.9.4', body: '' }, - { version: '0.9.5', body: 'invalid checksum' }, - ])('rejects invalid checksum $body', async ({ version, body }) => { - const { svc } = await toolContext(NubInstallService); - scope(baseUrl) - .get( - `/nubjs/nub/releases/download/v${version}/nub-linux-x64.tar.gz.sha256`, - ) - .reply(200, body); - - await expect(svc.install(version)).rejects.toThrow( - 'Invalid checksum for nub-linux-x64.tar.gz', - ); - }); - test('link', async () => { const { svc, pathSvc } = await toolContext(NubInstallService); const spy = vi.spyOn(LinkToolService.prototype, 'shellwrapper'); diff --git a/src/cli/tools/nub.ts b/src/cli/tools/nub.ts index e8885b1394..711552e643 100644 --- a/src/cli/tools/nub.ts +++ b/src/cli/tools/nub.ts @@ -1,4 +1,3 @@ -import fs from 'node:fs/promises'; import { join } from 'node:path'; import { injectFromHierarchy, injectable } from 'inversify'; import { BaseInstallService } from '../install-tool/base-install.service.ts'; @@ -22,13 +21,7 @@ export class NubInstallService extends BaseInstallService { const filename = `nub-linux-${this.ghArch}.tar.gz`; const url = `${baseUrl}${filename}`; - const checksumFile = await this.http.download({ url: `${url}.sha256` }); - const expectedChecksum = (await fs.readFile(checksumFile, 'utf-8')) - .trim() - .split(/\s+/)[0]; - if (!expectedChecksum || !/^[a-f0-9]{64}$/i.test(expectedChecksum)) { - throw new Error(`Invalid checksum for ${filename}`); - } + const expectedChecksum = await this.getChecksum(`${url}.sha256`); const file = await this.http.download({ url,