Skip to content

[Refactor] Extract managed-binary installation infrastructure - #1059

Open
navedmerchant wants to merge 2 commits into
mainfrom
feat/managed-binary-infrastructure
Open

[Refactor] Extract managed-binary installation infrastructure#1059
navedmerchant wants to merge 2 commits into
mainfrom
feat/managed-binary-infrastructure

Conversation

@navedmerchant

@navedmerchant navedmerchant commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Related GitHub Issue

Closes #1055
Part of #1049. Split from #1050.

Description

Extracts the binary lifecycle code previously embedded in the Semble downloader into reusable managed-binary services, then migrates Semble to those services without changing its behavior.

This PR is intentionally independent of DCG. It provides:

  • bounded downloads with cleanup on failure;
  • checksum verification;
  • safe ZIP/tar.gz extraction for a single expected binary;
  • staged, versioned installation with executable permissions;
  • safe handling of concurrent VS Code windows; and
  • focused managed-binary and Semble regression tests.

Stack

1 / 4 — base: main

Merge this PR first. The DCG service PR is stacked on top of this branch.

Test Procedure

pnpm --dir src exec vitest run services/managed-binary/__tests__/archive.spec.ts services/managed-binary/__tests__/download.spec.ts services/managed-binary/__tests__/install.spec.ts services/code-index/semble/__tests__/semble-downloader.spec.ts
pnpm --dir src check-types

Result: 4 test files passed, 46 tests passed; type-check passed. Repository pre-push type-check also passed.

Checklist

  • Focused issue and independently meaningful change
  • Tests included and passing
  • Type-check/lint passing
  • No changeset added
  • No user-facing documentation changes required

Summary by CodeRabbit

  • New Features

    • Added a shared managed-binary installation flow with version tracking and safer staged updates.
    • Improved support for downloading and extracting binary archives across supported platforms.
  • Bug Fixes

    • Improved handling of interrupted or failed installations by cleaning up temporary files.
    • Added stronger validation for archive contents, download destinations, redirects, file sizes, and checksums.
    • Prevented duplicate concurrent installation attempts and improved stale archive cleanup.

@navedmerchant navedmerchant added the enhancement New feature or request label Jul 30, 2026
@navedmerchant navedmerchant self-assigned this Jul 30, 2026
@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Shared managed-binary utilities now handle secure downloads, checksum verification, archive extraction, versioned atomic installation, cleanup, and concurrent installation control. The Semble downloader delegates to these utilities, and related tests and ESLint configuration were updated.

Changes

Managed binary infrastructure

Layer / File(s) Summary
Secure download and checksum utilities
src/services/managed-binary/download.ts, src/services/managed-binary/__tests__/download.spec.ts
Adds trusted HTTPS downloads with redirect validation, size limits, timeouts, exclusive writes, and streaming SHA-256 verification.
Archive extraction and layout validation
src/services/managed-binary/archive.ts, src/services/managed-binary/__tests__/archive.spec.ts
Adds process execution and platform-specific archive extraction with hardened tar options and single-file archive validation.
Versioned atomic installation
src/services/managed-binary/install.ts, src/services/managed-binary/__tests__/install.spec.ts
Adds staged installation, version tracking, executable handling, stale-archive cleanup, atomic promotion, validation, filesystem locking, and concurrent-install de-duplication.
Semble integration and regression coverage
src/services/code-index/semble/semble-downloader.ts, src/services/code-index/semble/__tests__/semble-downloader.spec.ts, src/eslint-suppressions.json
Migrates Semble to managed-binary utilities and updates cleanup, staging, lock, and lint-suppression expectations.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Semble as downloadSemble
  participant Installer as ensureManagedBinaryInstalled
  participant Downloader as downloadBinaryFile
  participant Checksum as verifySha256Checksum
  participant Extractor as extractTarGzArchive
  Semble->>Installer: provide version and lifecycle callbacks
  Installer->>Downloader: download trusted archive
  Installer->>Checksum: verify archive checksum
  Installer->>Extractor: extract into staging directory
  Installer-->>Semble: return installed binary path
Loading

Possibly related issues

Possibly related PRs

  • Zoo-Code-Org/Zoo-Code#734 — Modifies the Semble downloader and tests for archive downloads, checksum verification, versioned caching, and stale-archive cleanup.

Suggested labels: awaiting-review

Suggested reviewers: taltas

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.81% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the primary change: extracting reusable managed-binary installation infrastructure.
Description check ✅ Passed The description covers the linked issue, implementation, testing, scope, and checklist sufficiently for review.
Linked Issues check ✅ Passed The changes address the linked issue by adding shared download, extraction, verification, installation, cleanup, concurrency, and regression-test infrastructure.
Out of Scope Changes check ✅ Passed The changes remain focused on managed-binary infrastructure and Semble migration, with no unrelated DCG-specific functionality.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/managed-binary-infrastructure

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Jul 30, 2026

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (1)
src/services/managed-binary/archive.ts (1)

55-87: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Avoid constructing PowerShell scripts with string interpolation.

These extraction paths still interpolate user-controlled paths and archive names into PowerShell commands before shell escaping. Use PowerShell parameterized execution instead: pass values via -ArgumentList with a -File script, or build the command as script text and run it via -EncodedCommand so the values are not quoted into the command literal.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/managed-binary/archive.ts` around lines 55 - 87, Update
extractZipArchive and extractSingleFileZipArchive to stop interpolating archive
paths, destinations, expectedFile, or archiveName into PowerShell script text.
Pass these values as PowerShell parameters via -ArgumentList with a -File script
or an equivalent EncodedCommand approach, while preserving the existing
extraction and archive-layout validation behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/services/code-index/semble/__tests__/semble-downloader.spec.ts`:
- Around line 707-723: Replace the stale fs.unlink assertion in the upgrade
cleanup test with an assertion against fs.rm, verifying the unrelated file path
is not removed while preserving the existing force-option expectations for
intended archives.

In `@src/services/managed-binary/__tests__/archive.spec.ts`:
- Around line 61-79: Update the test “validates a single-file tar.xz layout
before extraction” to assert that extraction passes the exact archive member
name returned by the listing, including the “./” prefix. Adjust the mocked
listing or expected tar arguments only as needed to verify the implementation
preserves the stored member name rather than stripping it.

In `@src/services/managed-binary/archive.ts`:
- Around line 89-105: Preserve the raw tar member name in
extractSingleFileTarXzArchive for the extraction command, while normalizing a
separate value by removing ./ for the single-entry layout comparison. Update the
archive.spec.ts test expectation so a ./binary listing results in extraction
targeting ./binary; apply the change in both specified files.

In `@src/services/managed-binary/download.ts`:
- Around line 120-144: Update the maxBytes error path in the response data
handler to explicitly destroy or close the output stream before rejecting,
alongside response.destroy() and request.destroy(). Preserve the existing
successful output.on("finish") flow and ensure the stream’s file descriptor is
released when assertSizeWithinLimit throws.

In `@src/services/managed-binary/install.ts`:
- Line 4: Add cross-process locking to ensureManagedBinaryInstalled in addition
to the in-memory installationPromises map, using an exclusive lock file under
storageDir to serialize attempts targeting the deterministic staging path.
Ensure lock acquisition, release, and stale/error cleanup are handled safely,
and keep each owner’s archive and staging writes isolated until successful
promotion.

---

Nitpick comments:
In `@src/services/managed-binary/archive.ts`:
- Around line 55-87: Update extractZipArchive and extractSingleFileZipArchive to
stop interpolating archive paths, destinations, expectedFile, or archiveName
into PowerShell script text. Pass these values as PowerShell parameters via
-ArgumentList with a -File script or an equivalent EncodedCommand approach,
while preserving the existing extraction and archive-layout validation behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 1dfa2c7c-2e9f-416c-ba33-4eec94aae3ba

📥 Commits

Reviewing files that changed from the base of the PR and between c378193 and e93b1cc.

📒 Files selected for processing (9)
  • src/eslint-suppressions.json
  • src/services/code-index/semble/__tests__/semble-downloader.spec.ts
  • src/services/code-index/semble/semble-downloader.ts
  • src/services/managed-binary/__tests__/archive.spec.ts
  • src/services/managed-binary/__tests__/download.spec.ts
  • src/services/managed-binary/__tests__/install.spec.ts
  • src/services/managed-binary/archive.ts
  • src/services/managed-binary/download.ts
  • src/services/managed-binary/install.ts
💤 Files with no reviewable changes (1)
  • src/eslint-suppressions.json

Comment thread src/services/code-index/semble/__tests__/semble-downloader.spec.ts
Comment thread src/services/managed-binary/__tests__/archive.spec.ts
Comment thread src/services/managed-binary/archive.ts
Comment thread src/services/managed-binary/download.ts
Comment thread src/services/managed-binary/install.ts
@github-actions github-actions Bot added the awaiting-review PR changes are ready and waiting for maintainer re-review label Jul 30, 2026

@edelauna edelauna left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! Had a couple implementation comments.

Comment thread src/services/managed-binary/download.ts Outdated
Comment thread src/services/managed-binary/download.ts
Comment thread src/services/managed-binary/download.ts
Comment thread src/services/managed-binary/download.ts
Comment thread src/services/code-index/semble/semble-downloader.ts Outdated
Comment thread src/services/managed-binary/install.ts Outdated
Comment thread src/services/managed-binary/__tests__/install.spec.ts Outdated
Comment thread src/services/managed-binary/__tests__/install.spec.ts
Comment thread src/services/managed-binary/__tests__/archive.spec.ts
Comment thread src/services/managed-binary/__tests__/download.spec.ts
@github-actions github-actions Bot added awaiting-author PR is waiting for the author to address requested changes and removed awaiting-review PR changes are ready and waiting for maintainer re-review labels Jul 31, 2026
@github-actions github-actions Bot removed the awaiting-author PR is waiting for the author to address requested changes label Aug 1, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/services/managed-binary/__tests__/archive.spec.ts`:
- Around line 45-55: Wrap the fake-timer setup and test assertions in a
try/finally block within the “kills a process that exceeds its timeout” test,
and call vi.useRealTimers() in the finally block so timers are restored even
when an assertion fails.

In `@src/services/managed-binary/archive.ts`:
- Around line 109-116: Update the archive extraction flow around the TAR entry
validation and runProcess call to inspect entry types, accepting exactly one
regular file named expectedFile and rejecting symbolic links, hard links,
directories, and other non-regular entries. Apply the same ownership and
directory-overwrite protections already implemented by extractTarGzArchive
before extracting.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: dfecb8ff-1b98-44ef-af6f-4a38bc8f3e09

📥 Commits

Reviewing files that changed from the base of the PR and between e93b1cc and 24d527e.

📒 Files selected for processing (8)
  • src/services/code-index/semble/__tests__/semble-downloader.spec.ts
  • src/services/code-index/semble/semble-downloader.ts
  • src/services/managed-binary/__tests__/archive.spec.ts
  • src/services/managed-binary/__tests__/download.spec.ts
  • src/services/managed-binary/__tests__/install.spec.ts
  • src/services/managed-binary/archive.ts
  • src/services/managed-binary/download.ts
  • src/services/managed-binary/install.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/services/code-index/semble/tests/semble-downloader.spec.ts

Comment on lines +45 to +55
it("kills a process that exceeds its timeout", async () => {
vi.useFakeTimers()
const child = createChild()
mockSpawn.mockReturnValue(child as unknown as ReturnType<typeof spawn>)
const processResult = runProcess("tool", [], 100)
const assertion = expect(processResult).rejects.toThrow("tool timed out")

await vi.advanceTimersByTimeAsync(100)
await assertion
expect(child.kill).toHaveBeenCalledWith("SIGKILL")
vi.useRealTimers()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check for an existing global fake-timer cleanup hook in this spec file.
rg -n "afterEach|useRealTimers|useFakeTimers" src/services/managed-binary/__tests__/archive.spec.ts

Repository: Zoo-Code-Org/Zoo-Code

Length of output: 207


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "File size:"
wc -l src/services/managed-binary/__tests__/archive.spec.ts

echo
echo "Top 70 lines:"
sed -n '1,70p' src/services/managed-binary/__tests__/archive.spec.ts

echo
echo "All hook/timer references:"
rg -n "beforeEach|beforeAll|afterEach|afterAll|useRealTimers|useFakeTimers" src/services/managed-binary/__tests__/archive.spec.ts || true

echo
echo "Diff stat/name-status context:"
git diff --stat || true
git diff -- src/services/managed-binary/__tests__/archive.spec.ts | sed -n '1,220p' || true

Repository: Zoo-Code-Org/Zoo-Code

Length of output: 2639


Restore timers in a try/finally block.

vi.useRealTimers() is not part of a teardown hook, so an assertion failure here leaves fake timers active for later tests.

🔧 Proposed fix
 	it("kills a process that exceeds its timeout", async () => {
 		vi.useFakeTimers()
 		const child = createChild()
 		mockSpawn.mockReturnValue(child as unknown as ReturnType<typeof spawn>)
 		const processResult = runProcess("tool", [], 100)
 		const assertion = expect(processResult).rejects.toThrow("tool timed out")
 
-		await vi.advanceTimersByTimeAsync(100)
-		await assertion
-		expect(child.kill).toHaveBeenCalledWith("SIGKILL")
-		vi.useRealTimers()
+		try {
+			await vi.advanceTimersByTimeAsync(100)
+			await assertion
+			expect(child.kill).toHaveBeenCalledWith("SIGKILL")
+		} finally {
+			vi.useRealTimers()
+		}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it("kills a process that exceeds its timeout", async () => {
vi.useFakeTimers()
const child = createChild()
mockSpawn.mockReturnValue(child as unknown as ReturnType<typeof spawn>)
const processResult = runProcess("tool", [], 100)
const assertion = expect(processResult).rejects.toThrow("tool timed out")
await vi.advanceTimersByTimeAsync(100)
await assertion
expect(child.kill).toHaveBeenCalledWith("SIGKILL")
vi.useRealTimers()
it("kills a process that exceeds its timeout", async () => {
vi.useFakeTimers()
const child = createChild()
mockSpawn.mockReturnValue(child as unknown as ReturnType<typeof spawn>)
const processResult = runProcess("tool", [], 100)
const assertion = expect(processResult).rejects.toThrow("tool timed out")
try {
await vi.advanceTimersByTimeAsync(100)
await assertion
expect(child.kill).toHaveBeenCalledWith("SIGKILL")
} finally {
vi.useRealTimers()
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/managed-binary/__tests__/archive.spec.ts` around lines 45 - 55,
Wrap the fake-timer setup and test assertions in a try/finally block within the
“kills a process that exceeds its timeout” test, and call vi.useRealTimers() in
the finally block so timers are restored even when an assertion fails.

Comment on lines +109 to +116
.map((entry) => entry.trim())
.filter(Boolean)
const archiveEntry = entries[0]
if (entries.length !== 1 || archiveEntry.replace(/^\.\//, "") !== expectedFile) {
throw new Error(`${archiveName} archive has an unexpected layout`)
}

await runProcess("tar", ["-xJf", archivePath, "-C", destination, archiveEntry])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Reject non-regular TAR entries before extraction.

Line 106 lists only member names. A one-entry archive with a symbolic link or hard link named expectedFile passes this check. Line 116 then extracts the link into staging.

Inspect the TAR entry type and accept only one regular file. Apply the ownership and directory-overwrite protections used by extractTarGzArchive to this extraction path.

🧰 Tools
🪛 ast-grep (0.45.0)

[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { spawn } from "child_process"
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(detect-child-process-typescript)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/managed-binary/archive.ts` around lines 109 - 116, Update the
archive extraction flow around the TAR entry validation and runProcess call to
inspect entry types, accepting exactly one regular file named expectedFile and
rejecting symbolic links, hard links, directories, and other non-regular
entries. Apply the same ownership and directory-overwrite protections already
implemented by extractTarGzArchive before extracting.

@github-actions github-actions Bot added the awaiting-author PR is waiting for the author to address requested changes label Aug 1, 2026

@edelauna edelauna left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Had some questions about the implementation.

}
})
response.on("error", reject)
response.pipe(output)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be safer to also destroy the streams here? If the response emits an error, output and request stay open — compare the size-limit handler at lines 133–138, which calls output.destroy(), response.destroy(), and request.destroy() before rejecting. Should this handler follow the same pattern to avoid socket leaks?

resolve()
})
output.on("error", reject)
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same question: if output errors after piping has started, should we also destroy response and request to release the underlying socket and in-flight HTTP connection?

input.emit("data", Buffer.from("archive"))
input.emit("end")
await expect(verification).rejects.toThrow("checksum mismatch: actual-checksum")
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a test for the happy path here — where the computed digest actually equals expected and verifyFileChecksum resolves without throwing? The suite currently only exercises the mismatch branch.

mockSpawn.mockReturnValueOnce(extraction as unknown as ReturnType<typeof spawn>)
const result = extractSingleFileTarXzArchive("/tmp/archive.tar.xz", "/tmp/output", "binary", "Tool")
listing.stdout.write("./binary\n")
listing.emit("close", 0)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feeds the happy path (single ./binary entry). Are there tests for the error branches — e.g. writing './binary\n./other\n' to listing.stdout (multi-entry) or a filename that doesn't match info.binary (wrong-filename error at archive.ts:112)?

update: 30_000,
retries: { retries: 10, factor: 1.5, minTimeout: 100, maxTimeout: 1_000 },
})
try {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason not to pass an onCompromised callback here? safeWriteJson.ts uses one so that stale-lock detection failures surface loudly rather than silently continuing — should the same policy apply to the binary install lock?

}

export async function extractSingleFileZipArchive(
archivePath: string,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this have a process.platform === 'win32' guard, or at least a JSDoc note that it only works on Windows? PowerShell is invoked unconditionally, so calling this function on Linux/macOS will produce a confusing error.

await vi.waitFor(() => expect(download).toHaveBeenCalledOnce())
finishDownload?.()
await Promise.all([first, second])
expect(download).toHaveBeenCalledOnce()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are the resolved values from both calls asserted? A mutation that returns the wrong path would still pass if only the settlement of Promise.all is tested, not the actual paths resolved by first and second.

name: "Semble",
trustedDomains: TRUSTED_DOWNLOAD_DOMAINS,
timeoutMs: 120_000,
}),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR description highlights bounded downloads as a key property of downloadBinaryFile — does omitting maxBytes here leave the Semble download unbounded? Should info.size (or a conservative upper limit) be threaded through?

if (info.archive.endsWith(".tar.gz")) {
await extractTarGzArchive(archivePath, stagingDir)
} else if (info.archive.endsWith(".zip")) {
await extractZipArchive(archivePath, stagingDir)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If info.archive is neither .tar.gz nor .zip, this falls through silently — the archive is downloaded but never extracted and binaryPath won't exist. Should an unrecognised format throw instead of no-op?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

awaiting-author PR is waiting for the author to address requested changes enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Extract reusable managed-binary installation infrastructure

2 participants