From 87b1121f6c2a892e628efc9c12983b4e96f72d69 Mon Sep 17 00:00:00 2001 From: Leavi15 Date: Thu, 16 Apr 2026 14:39:28 -0600 Subject: [PATCH 01/25] feat(config): improve libkrunfw path resolution with additional checks and tests --- crates/microsandbox/lib/config/mod.rs | 69 +++++++++++++++++++++++++-- 1 file changed, 65 insertions(+), 4 deletions(-) diff --git a/crates/microsandbox/lib/config/mod.rs b/crates/microsandbox/lib/config/mod.rs index 982fd548..0c19a689 100644 --- a/crates/microsandbox/lib/config/mod.rs +++ b/crates/microsandbox/lib/config/mod.rs @@ -667,8 +667,9 @@ pub fn resolve_msb_path() -> MicrosandboxResult { /// Resolution order: /// 1. `config().paths.libkrunfw` /// 2. A sibling of the resolved `msb` binary (for `build/msb`) -/// 3. `../lib/` next to the resolved `msb` binary (for installed layouts) -/// 4. `{home}/lib/libkrunfw.{so,dylib}` +/// 3. `../lib/microsandbox/` next to the resolved `msb` binary +/// 4. `../lib/` next to the resolved `msb` binary (for installed layouts) +/// 5. `{home}/lib/libkrunfw.{so,dylib}` pub fn resolve_libkrunfw_path() -> MicrosandboxResult { if let Some(path) = &config().paths.libkrunfw { if path.is_file() { @@ -697,9 +698,9 @@ pub fn resolve_libkrunfw_path() -> MicrosandboxResult { } candidates.push(home_fallback); - if let Some(path) = candidates.iter().find(|path| path.is_file()) { + if let Some(path) = first_existing_path(&candidates) { tracing::debug!(path = %path.display(), "resolved libkrunfw path"); - return Ok(path.clone()); + return Ok(path); } let searched = candidates @@ -718,6 +719,17 @@ fn libkrunfw_candidates_from_msb(msb_path: &Path, filename: &str) -> Vec Vec Option { + candidates.iter().find(|path| path.is_file()).cloned() +} + #[cfg(debug_assertions)] fn dev_msb_candidates_from(start: &Path) -> Vec { let mut candidates = Vec::new(); @@ -1156,6 +1172,51 @@ mod tests { assert_eq!(paths.len(), 2); } + #[test] + fn test_libkrunfw_candidates_for_packaged_msb() { + let msb = PathBuf::from("/usr/bin/msb"); + let paths = libkrunfw_candidates_from_msb(&msb, "libkrunfw.so.5.2.1"); + assert_eq!(paths[0], PathBuf::from("/usr/bin/libkrunfw.so.5.2.1")); + assert_eq!( + paths[1], + PathBuf::from("/usr/lib/microsandbox/libkrunfw.so.5.2.1") + ); + assert_eq!(paths[2], PathBuf::from("/usr/lib/libkrunfw.so.5.2.1")); + assert_eq!(paths.len(), 3); + } + + #[test] + fn test_first_existing_path_prefers_packaged_layout_candidate() { + let temp = tempfile::tempdir().unwrap(); + let packaged = temp.path().join("usr/lib/microsandbox/libkrunfw.so.5.2.1"); + let home = temp + .path() + .join("home/.microsandbox/lib/libkrunfw.so.5.2.1"); + + std::fs::create_dir_all(packaged.parent().unwrap()).unwrap(); + std::fs::create_dir_all(home.parent().unwrap()).unwrap(); + std::fs::write(&packaged, b"packaged").unwrap(); + std::fs::write(&home, b"home").unwrap(); + + let resolved = first_existing_path(&[packaged.clone(), home.clone()]); + assert_eq!(resolved, Some(packaged)); + } + + #[test] + fn test_first_existing_path_falls_back_when_packaged_candidate_missing() { + let temp = tempfile::tempdir().unwrap(); + let missing = temp.path().join("usr/lib/microsandbox/libkrunfw.so.5.2.1"); + let home = temp + .path() + .join("home/.microsandbox/lib/libkrunfw.so.5.2.1"); + + std::fs::create_dir_all(home.parent().unwrap()).unwrap(); + std::fs::write(&home, b"home").unwrap(); + + let resolved = first_existing_path(&[missing, home.clone()]); + assert_eq!(resolved, Some(home)); + } + #[test] fn test_dev_msb_candidates_from_workspace_root() { let temp = tempfile::tempdir().unwrap(); From 0dd2adc729b68d63e9f0c7e02a9d5aa72aa7baf5 Mon Sep 17 00:00:00 2001 From: Leavi15 Date: Thu, 16 Apr 2026 14:41:00 -0600 Subject: [PATCH 02/25] feat(cli): add APT-managed installation detection and update hints --- crates/cli/lib/commands/self_cmd.rs | 249 ++++++++++++++++++++++++++++ 1 file changed, 249 insertions(+) diff --git a/crates/cli/lib/commands/self_cmd.rs b/crates/cli/lib/commands/self_cmd.rs index 72822b53..8356523a 100644 --- a/crates/cli/lib/commands/self_cmd.rs +++ b/crates/cli/lib/commands/self_cmd.rs @@ -3,6 +3,11 @@ use std::io::Write; use std::path::{Path, PathBuf}; +#[cfg(target_os = "linux")] +use std::io::ErrorKind; +#[cfg(target_os = "linux")] +use std::process::Command; + use clap::{Args, Subcommand}; use console::{Key, Term, style}; @@ -14,6 +19,7 @@ use crate::ui; //-------------------------------------------------------------------------------------------------- const CURRENT_VERSION: &str = env!("CARGO_PKG_VERSION"); +const APT_PACKAGE_NAME: &str = "microsandbox"; const MARKER_START: &str = "# >>> microsandbox >>>"; const MARKER_END: &str = "# <<< microsandbox <<<"; @@ -70,6 +76,18 @@ enum UninstallCategory { Secrets, } +#[derive(Debug, Clone, PartialEq, Eq)] +struct AptManagedInstallation { + package: String, +} + +#[cfg(target_os = "linux")] +#[derive(Debug, Clone, PartialEq, Eq)] +enum DpkgQueryError { + CommandUnavailable, + CommandFailed, +} + impl UninstallCategory { const ITEMS: &[Self] = &[ Self::All, @@ -124,6 +142,11 @@ pub async fn run(args: SelfArgs) -> anyhow::Result<()> { async fn run_update(args: SelfUpdateArgs) -> anyhow::Result<()> { info(&format!("Current version: v{CURRENT_VERSION}")); + if let Some(installation) = detect_apt_managed_installation() { + print_apt_managed_update_notice(&installation); + return Ok(()); + } + let spinner = ui::Spinner::start("Checking", "latest release"); let latest = fetch_latest_version().await?; spinner.finish_clear(); @@ -165,6 +188,11 @@ async fn run_update(args: SelfUpdateArgs) -> anyhow::Result<()> { } async fn run_uninstall(args: SelfUninstallArgs) -> anyhow::Result<()> { + if let Some(installation) = detect_apt_managed_installation() { + print_apt_managed_uninstall_notice(&installation); + return Ok(()); + } + let base_dir = resolve_base_dir()?; if !base_dir.exists() { @@ -416,6 +444,105 @@ fn done(msg: &str) { eprintln!("{} {msg}", style("done").green().bold()); } +#[cfg(target_os = "linux")] +fn detect_apt_managed_installation() -> Option { + let current_exe = std::env::current_exe().ok()?; + + detect_apt_managed_installation_with(¤t_exe, |executable_path| { + let output = Command::new("dpkg-query") + .args(["-S", &executable_path.display().to_string()]) + .output(); + + match output { + Ok(output) if output.status.success() => { + Ok(String::from_utf8_lossy(&output.stdout).into_owned()) + } + Ok(_) => Err(DpkgQueryError::CommandFailed), + Err(error) if error.kind() == ErrorKind::NotFound => { + Err(DpkgQueryError::CommandUnavailable) + } + Err(_) => Err(DpkgQueryError::CommandFailed), + } + }) +} + +#[cfg(not(target_os = "linux"))] +fn detect_apt_managed_installation() -> Option { + None +} + +#[cfg(target_os = "linux")] +fn detect_apt_managed_installation_with( + executable_path: &Path, + query: F, +) -> Option +where + F: FnOnce(&Path) -> Result, +{ + match query(executable_path) { + Ok(stdout) => parse_apt_managed_installation(APT_PACKAGE_NAME, executable_path, &stdout), + Err(DpkgQueryError::CommandUnavailable | DpkgQueryError::CommandFailed) => None, + } +} + +#[cfg(target_os = "linux")] +fn parse_apt_managed_installation( + package: &str, + executable_path: &Path, + stdout: &str, +) -> Option { + let expected_path = executable_path.display().to_string(); + + stdout.lines().find_map(|line| { + let (package_field, owned_path) = line.rsplit_once(": ")?; + if owned_path.trim() != expected_path { + return None; + } + + package_field.split(',').find_map(|entry| { + let package_name = entry.trim().split(':').next()?; + (package_name == package).then(|| AptManagedInstallation { + package: package.to_string(), + }) + }) + }) +} + +fn apt_upgrade_hint(installation: &AptManagedInstallation) -> String { + format!( + "sudo apt update && sudo apt upgrade {}", + installation.package + ) +} + +fn apt_remove_hint(installation: &AptManagedInstallation) -> String { + format!("sudo apt remove {}", installation.package) +} + +fn apt_purge_hint(installation: &AptManagedInstallation) -> String { + format!("sudo apt purge {}", installation.package) +} + +fn print_apt_managed_update_notice(installation: &AptManagedInstallation) { + info("This microsandbox installation is managed by APT."); + info(&format!( + "Run `{}` to upgrade the system package.", + apt_upgrade_hint(installation) + )); +} + +fn print_apt_managed_uninstall_notice(installation: &AptManagedInstallation) { + info("This microsandbox installation is managed by APT."); + info(&format!( + "Run `{}` to uninstall the system package.", + apt_remove_hint(installation) + )); + info(&format!( + "Use `{}` if you also want to remove package metadata.", + apt_purge_hint(installation) + )); +} + /// Remove a single uninstall category from the base directory. fn remove_category(base_dir: &Path, category: UninstallCategory) -> anyhow::Result<()> { match category { @@ -525,3 +652,125 @@ fn remove_marker_block(path: &Path) -> anyhow::Result { std::fs::write(path, result)?; Ok(true) } + +#[cfg(test)] +mod tests { + use super::*; + + #[cfg(target_os = "linux")] + #[test] + fn test_parse_apt_managed_installation_accepts_package_owner_for_current_executable() { + let executable = PathBuf::from("/usr/bin/msb"); + let installation = parse_apt_managed_installation( + APT_PACKAGE_NAME, + &executable, + "microsandbox: /usr/bin/msb\n", + ); + assert_eq!( + installation, + Some(AptManagedInstallation { + package: "microsandbox".to_string(), + }) + ); + } + + #[cfg(target_os = "linux")] + #[test] + fn test_parse_apt_managed_installation_accepts_arch_qualified_package_owner() { + let executable = PathBuf::from("/usr/bin/msb"); + let installation = parse_apt_managed_installation( + APT_PACKAGE_NAME, + &executable, + "microsandbox:amd64: /usr/bin/msb\n", + ); + assert_eq!( + installation, + Some(AptManagedInstallation { + package: "microsandbox".to_string(), + }) + ); + } + + #[cfg(target_os = "linux")] + #[test] + fn test_parse_apt_managed_installation_rejects_different_package_owner() { + let executable = PathBuf::from("/usr/bin/msb"); + let installation = parse_apt_managed_installation( + APT_PACKAGE_NAME, + &executable, + "other-package: /usr/bin/msb\n", + ); + assert_eq!(installation, None); + } + + #[cfg(target_os = "linux")] + #[test] + fn test_parse_apt_managed_installation_rejects_different_owned_path() { + let executable = PathBuf::from("/home/user/.microsandbox/bin/msb"); + let installation = parse_apt_managed_installation( + APT_PACKAGE_NAME, + &executable, + "microsandbox: /usr/bin/msb\n", + ); + assert_eq!(installation, None); + } + + #[cfg(target_os = "linux")] + #[test] + fn test_detect_apt_managed_installation_ignores_missing_dpkg_query() { + let executable = PathBuf::from("/usr/bin/msb"); + let installation = detect_apt_managed_installation_with(&executable, |_| { + Err(DpkgQueryError::CommandUnavailable) + }); + assert_eq!(installation, None); + } + + #[cfg(target_os = "linux")] + #[test] + fn test_detect_apt_managed_installation_ignores_failed_query() { + let executable = PathBuf::from("/usr/bin/msb"); + let installation = detect_apt_managed_installation_with(&executable, |_| { + Err(DpkgQueryError::CommandFailed) + }); + assert_eq!(installation, None); + } + + #[cfg(target_os = "linux")] + #[test] + fn test_detect_apt_managed_installation_with_successful_owner_query() { + let executable = PathBuf::from("/usr/bin/msb"); + let installation = detect_apt_managed_installation_with(&executable, |path| { + Ok(format!("microsandbox: {}\n", path.display())) + }); + assert_eq!( + installation, + Some(AptManagedInstallation { + package: "microsandbox".to_string(), + }) + ); + } + + #[cfg(not(target_os = "linux"))] + #[test] + fn test_detect_apt_managed_installation_returns_none_off_linux() { + assert_eq!(detect_apt_managed_installation(), None); + } + + #[cfg(target_os = "linux")] + #[test] + fn test_apt_hints_use_expected_package_name() { + let installation = AptManagedInstallation { + package: "microsandbox".to_string(), + }; + + assert_eq!( + apt_upgrade_hint(&installation), + "sudo apt update && sudo apt upgrade microsandbox" + ); + assert_eq!( + apt_remove_hint(&installation), + "sudo apt remove microsandbox" + ); + assert_eq!(apt_purge_hint(&installation), "sudo apt purge microsandbox"); + } +} From 0c48354a23b1aa0c1fe2db392301eca1de7fba3b Mon Sep 17 00:00:00 2001 From: Leavi15 Date: Thu, 16 Apr 2026 14:44:20 -0600 Subject: [PATCH 03/25] feat(docs): clarify behavior of APT-managed `msb` installation commands --- docs/cli/sandbox-commands.mdx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/cli/sandbox-commands.mdx b/docs/cli/sandbox-commands.mdx index 0a8dbabe..0825a486 100644 --- a/docs/cli/sandbox-commands.mdx +++ b/docs/cli/sandbox-commands.mdx @@ -317,6 +317,11 @@ msb self uninstall # Remove msb (with confirmation prompt) msb self uninstall --yes # Skip confirmation ``` +> If `msb` was installed from the official APT repository, these commands do +> not modify the system package directly. They print the equivalent `apt` +> command instead: `apt upgrade microsandbox`, `apt remove microsandbox`, or +> `apt purge microsandbox`. + | Subcommand | Description | |------------|-------------| | `update` (alias: `upgrade`) | Update msb and libkrunfw to the latest release | From e7edab68557c1754473abda9b3278ba4a11b443a Mon Sep 17 00:00:00 2001 From: Leavi15 Date: Thu, 16 Apr 2026 15:19:28 -0600 Subject: [PATCH 04/25] feat(ci): add APT package validation and smoke test workflows --- .github/workflows/check.yml | 136 ++++++++++++++++++++++++++++++++++++ 1 file changed, 136 insertions(+) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index b0177749..3cbd657f 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -317,6 +317,112 @@ jobs: npm install --no-package-lock --ignore-scripts npm run build + # --------------------------------------------------------------------------- + # Validate APT packages and repository + # --------------------------------------------------------------------------- + apt-package-test: + name: APT Package Validation (${{ matrix.target }}) + needs: check + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: false + matrix: + include: + - target: linux-x86_64 + runner: ubuntu-latest + arch: x86_64 + deb_arch: amd64 + upload_kvm_repo: true + - target: linux-aarch64 + runner: ubuntu-24.04-arm + arch: aarch64 + deb_arch: arm64 + upload_kvm_repo: false + steps: + - uses: actions/checkout@v4 + + - name: Install APT packaging dependencies + run: | + sudo apt-get update + sudo apt-get install -y dpkg-dev apt-utils lintian gnupg + + - name: Build baseline APT artifacts + run: | + bash scripts/build-apt-baseline-artifacts.sh \ + --output-dir build/apt/${{ matrix.arch }} + + - name: Build and validate .deb packages + run: | + VERSION="$(sed -n 's/^version = \"\\(.*\\)\"$/\\1/p' Cargo.toml | head -n1)" + + bash scripts/package-deb.sh \ + --arch "${{ matrix.arch }}" \ + --version "$VERSION" \ + --revision 1 \ + --msb build/apt/${{ matrix.arch }}/msb \ + --libkrunfw build/apt/${{ matrix.arch }}/libkrunfw.so.${{ env.LIBKRUNFW_VERSION }} \ + --output-dir dist/v1 + + bash scripts/package-deb.sh \ + --arch "${{ matrix.arch }}" \ + --version "$VERSION" \ + --revision 2 \ + --msb build/apt/${{ matrix.arch }}/msb \ + --libkrunfw build/apt/${{ matrix.arch }}/libkrunfw.so.${{ env.LIBKRUNFW_VERSION }} \ + --output-dir dist/v2 + + bash scripts/validate-deb.sh \ + --deb dist/v1/microsandbox_*_${{ matrix.deb_arch }}.deb \ + --arch "${{ matrix.deb_arch }}" \ + --version "$VERSION" \ + --revision 1 + + bash scripts/validate-deb.sh \ + --deb dist/v2/microsandbox_*_${{ matrix.deb_arch }}.deb \ + --arch "${{ matrix.deb_arch }}" \ + --version "$VERSION" \ + --revision 2 + + - name: Build signed test repositories + env: + GNUPGHOME: ${{ runner.temp }}/apt-gnupg + run: | + mkdir -p "$GNUPGHOME" + chmod 700 "$GNUPGHOME" + + GOOD_KEY_ID="$(bash scripts/generate-apt-test-key.sh --gnupg-home "$GNUPGHOME")" + BAD_KEY_ID="$(bash scripts/generate-apt-test-key.sh \ + --gnupg-home "$GNUPGHOME" \ + --name-real "Microsandbox Wrong Repository" \ + --name-email "wrong@microsandbox.dev")" + + bash scripts/build-apt-repo.sh \ + --input-dir dist/v1 \ + --output-dir apt-repo-v1 \ + --gpg-key-id "$GOOD_KEY_ID" + + bash scripts/build-apt-repo.sh \ + --input-dir dist/v2 \ + --output-dir apt-repo-v2 \ + --gpg-key-id "$GOOD_KEY_ID" + + gpg --batch --yes --output wrong-repo-keyring.gpg --export "$BAD_KEY_ID" + + - name: Test APT install and upgrade in containers + run: | + bash scripts/test-apt-repo.sh \ + --repo-v1 apt-repo-v1 \ + --repo-v2 apt-repo-v2 \ + --keyring apt-repo-v1/microsandbox-archive-keyring.gpg \ + --bad-keyring wrong-repo-keyring.gpg + + - name: Upload signed APT repository artifact + if: matrix.upload_kvm_repo + uses: actions/upload-artifact@v4 + with: + name: apt-repo-v1-${{ matrix.target }} + path: apt-repo-v1/ + # --------------------------------------------------------------------------- # Integration tests (requires KVM) # --------------------------------------------------------------------------- @@ -433,3 +539,33 @@ jobs: export PATH="$HOME/.microsandbox/bin:$PATH" export LD_LIBRARY_PATH="${{ github.workspace }}/build:$HOME/.microsandbox/lib" npm test + + # --------------------------------------------------------------------------- + # APT smoke tests (requires KVM) + # --------------------------------------------------------------------------- + apt-kvm-smoke: + name: APT KVM Smoke Test + needs: apt-package-test + runs-on: self-hosted-ubuntu-2404-x64 + steps: + - uses: actions/checkout@v4 + + - name: Clean workspace + run: | + rm -rf "${{ github.workspace }}"/apt-repo + rm -rf ~/.microsandbox + sudo apt-get remove -y microsandbox || true + sudo rm -f /etc/apt/sources.list.d/microsandbox.list + sudo rm -f /usr/share/keyrings/microsandbox-archive-keyring.gpg + + - name: Download signed APT repository + uses: actions/download-artifact@v4 + with: + name: apt-repo-v1-linux-x86_64 + path: apt-repo/ + + - name: Install and smoke test microsandbox from APT + run: | + bash scripts/apt-smoke-test.sh \ + --repo-url "file://${{ github.workspace }}/apt-repo" \ + --keyring-path "${{ github.workspace }}/apt-repo/microsandbox-archive-keyring.gpg" From ee588ddd238411a148e894a2855ca733eec1fae9 Mon Sep 17 00:00:00 2001 From: Leavi15 Date: Thu, 16 Apr 2026 15:25:18 -0600 Subject: [PATCH 05/25] feat(ci): add release metadata generation and upload steps --- .github/workflows/check.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 3cbd657f..8c068d70 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -304,6 +304,19 @@ jobs: sdk/node-ts/tsconfig.json sdk/node-ts/vitest.config.ts + - name: Write release metadata + if: matrix.target == 'linux-x86_64' && startsWith(github.ref, 'refs/tags/v') + run: | + mkdir -p release-metadata + printf '%s\n' "${GITHUB_REF_NAME}" > release-metadata/release-tag.txt + + - name: Upload release metadata + if: matrix.target == 'linux-x86_64' && startsWith(github.ref, 'refs/tags/v') + uses: actions/upload-artifact@v4 + with: + name: release-metadata + path: release-metadata/release-tag.txt + # -- Checks (MCP server) -- - name: Build MCP server working-directory: mcp From 376fec4b59d25474610fbe874da94fabf521385f Mon Sep 17 00:00:00 2001 From: Leavi15 Date: Thu, 16 Apr 2026 15:30:16 -0600 Subject: [PATCH 06/25] feat(ci): add workflows for APT packaging and repository deployment --- .github/workflows/release.yml | 206 ++++++++++++++++++++++++++++++++-- 1 file changed, 196 insertions(+), 10 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2f767324..0f2b24a2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,21 +8,64 @@ env: CARGO_TERM_COLOR: always LIBKRUNFW_VERSION: "5.2.1" LIBKRUNFW_ABI: "5" + APT_REPO_DOMAIN: "apt.microsandbox.dev" permissions: + actions: read contents: write packages: write + pages: write + id-token: write jobs: + # --------------------------------------------------------------------------- + # Prepare release context from the successful Check workflow run + # --------------------------------------------------------------------------- + prepare: + name: Prepare Release Context + if: github.event.workflow_run.conclusion == 'success' + runs-on: ubuntu-latest + outputs: + release_tag: ${{ steps.context.outputs.release_tag }} + source_sha: ${{ steps.context.outputs.source_sha }} + steps: + - name: Resolve release tag + id: context + env: + GH_TOKEN: ${{ github.token }} + run: | + SOURCE_SHA="${{ github.event.workflow_run.head_sha }}" + RELEASE_TAG="" + + mkdir -p release-metadata + if gh run download "${{ github.event.workflow_run.id }}" \ + --repo "${{ github.repository }}" \ + --name release-metadata \ + --dir release-metadata; then + RELEASE_TAG="$(tr -d '\n' < release-metadata/release-tag.txt)" + fi + + echo "source_sha=$SOURCE_SHA" >> "$GITHUB_OUTPUT" + echo "release_tag=$RELEASE_TAG" >> "$GITHUB_OUTPUT" + + if [ -n "$RELEASE_TAG" ]; then + echo "Preparing release for $RELEASE_TAG from Check run ${{ github.event.workflow_run.id }}" + else + echo "Check succeeded without release metadata; skipping release workflow." + fi + # --------------------------------------------------------------------------- # Build kernel.c on Linux for macOS libkrunfw linking # --------------------------------------------------------------------------- build-kernel: name: Build kernel.c (aarch64) + needs: prepare + if: needs.prepare.outputs.release_tag != '' runs-on: ubuntu-24.04-arm steps: - uses: actions/checkout@v4 with: + ref: ${{ needs.prepare.outputs.source_sha }} submodules: true - name: Cache kernel.c @@ -53,10 +96,13 @@ jobs: # --------------------------------------------------------------------------- build-agentd-aarch64: name: Build agentd (aarch64-linux-musl) + needs: prepare + if: needs.prepare.outputs.release_tag != '' runs-on: ubuntu-24.04-arm steps: - uses: actions/checkout@v4 with: + ref: ${{ needs.prepare.outputs.source_sha }} submodules: true - uses: dtolnay/rust-toolchain@stable @@ -80,12 +126,12 @@ jobs: path: build/agentd # --------------------------------------------------------------------------- - # Build + # Build release artifacts # --------------------------------------------------------------------------- build: name: Build (${{ matrix.target }}) - needs: [build-kernel, build-agentd-aarch64] - if: always() + needs: [prepare, build-kernel, build-agentd-aarch64] + if: needs.prepare.outputs.release_tag != '' runs-on: ${{ matrix.runner }} strategy: fail-fast: false @@ -125,6 +171,7 @@ jobs: steps: - uses: actions/checkout@v4 with: + ref: ${{ needs.prepare.outputs.source_sha }} submodules: true - uses: dtolnay/rust-toolchain@stable @@ -288,15 +335,142 @@ jobs: name: release-${{ matrix.target }} path: artifacts/ + # --------------------------------------------------------------------------- + # Package Debian artifacts for release + # --------------------------------------------------------------------------- + apt-package: + name: Package APT (${{ matrix.target }}) + needs: prepare + if: needs.prepare.outputs.release_tag != '' + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: false + matrix: + include: + - target: linux-x86_64 + runner: ubuntu-latest + arch: x86_64 + - target: linux-aarch64 + runner: ubuntu-24.04-arm + arch: aarch64 + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ needs.prepare.outputs.source_sha }} + + - name: Install APT packaging dependencies + run: | + sudo apt-get update + sudo apt-get install -y dpkg-dev apt-utils gnupg + + - name: Build baseline APT artifacts + run: | + bash scripts/build-apt-baseline-artifacts.sh \ + --output-dir build/apt/${{ matrix.arch }} + + - name: Package Debian archive + run: | + bash scripts/package-deb.sh \ + --arch "${{ matrix.arch }}" \ + --version "${{ needs.prepare.outputs.release_tag }}" \ + --revision 1 \ + --msb build/apt/${{ matrix.arch }}/msb \ + --libkrunfw build/apt/${{ matrix.arch }}/libkrunfw.so.${{ env.LIBKRUNFW_VERSION }} \ + --output-dir dist + + - name: Upload Debian package artifact + uses: actions/upload-artifact@v4 + with: + name: release-apt-${{ matrix.target }} + path: dist/*.deb + + # --------------------------------------------------------------------------- + # Build and sign the APT repository + # --------------------------------------------------------------------------- + apt-repo: + name: Build APT Repository + needs: [prepare, apt-package] + if: needs.prepare.outputs.release_tag != '' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ needs.prepare.outputs.source_sha }} + + - name: Install APT repository tooling + run: | + sudo apt-get update + sudo apt-get install -y dpkg-dev apt-utils gnupg + + - name: Download Debian packages + uses: actions/download-artifact@v4 + with: + path: release-artifacts + pattern: release-apt-* + merge-multiple: true + + - name: Import APT signing key + id: apt-key + env: + GNUPGHOME: ${{ runner.temp }}/apt-gnupg + APT_GPG_PRIVATE_KEY: ${{ secrets.APT_GPG_PRIVATE_KEY }} + APT_GPG_PRIVATE_KEY_BASE64: ${{ secrets.APT_GPG_PRIVATE_KEY_BASE64 }} + run: | + mkdir -p "$GNUPGHOME" + chmod 700 "$GNUPGHOME" + KEY_ID="$(bash scripts/import-apt-signing-key.sh --gnupg-home "$GNUPGHOME")" + echo "key_id=$KEY_ID" >> "$GITHUB_OUTPUT" + + - name: Build signed APT repository + env: + GNUPGHOME: ${{ runner.temp }}/apt-gnupg + APT_GPG_PASSPHRASE: ${{ secrets.APT_GPG_PASSPHRASE }} + run: | + bash scripts/build-apt-repo.sh \ + --input-dir release-artifacts \ + --output-dir apt-repo \ + --gpg-key-id "${{ steps.apt-key.outputs.key_id }}" + + printf '%s\n' "${{ env.APT_REPO_DOMAIN }}" > apt-repo/CNAME + + - name: Upload APT repository artifact + uses: actions/upload-artifact@v4 + with: + name: apt-repo + path: apt-repo/ + + - name: Upload Pages artifact + uses: actions/upload-pages-artifact@v3 + with: + path: apt-repo/ + + # --------------------------------------------------------------------------- + # Publish APT repository to GitHub Pages + # --------------------------------------------------------------------------- + deploy-apt-repo: + name: Deploy APT Repository + needs: [prepare, apt-repo] + if: needs.prepare.outputs.release_tag != '' + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - id: deployment + uses: actions/deploy-pages@v4 + # --------------------------------------------------------------------------- # Assemble: collect all artifacts, generate checksums, create GitHub release # --------------------------------------------------------------------------- assemble: name: Assemble Release - needs: build + needs: [prepare, build, apt-package, deploy-apt-repo] + if: needs.prepare.outputs.release_tag != '' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + ref: ${{ needs.prepare.outputs.source_sha }} - name: Download all artifacts uses: actions/download-artifact@v4 @@ -317,8 +491,8 @@ jobs: env: GH_TOKEN: ${{ github.token }} run: | - gh release create "${{ github.ref_name }}" \ - --title "${{ github.ref_name }}" \ + gh release create "${{ needs.prepare.outputs.release_tag }}" \ + --title "${{ needs.prepare.outputs.release_tag }}" \ --generate-notes \ release-artifacts/* @@ -342,10 +516,13 @@ jobs: # --------------------------------------------------------------------------- npm-publish: name: Publish npm packages - needs: build + needs: [prepare, build] + if: needs.prepare.outputs.release_tag != '' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + ref: ${{ needs.prepare.outputs.source_sha }} - uses: actions/setup-node@v4 with: @@ -448,11 +625,13 @@ jobs: # --------------------------------------------------------------------------- mcp-publish: name: Publish MCP server - needs: npm-publish + needs: [prepare, npm-publish] + if: needs.prepare.outputs.release_tag != '' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: + ref: ${{ needs.prepare.outputs.source_sha }} submodules: true - uses: actions/setup-node@v4 @@ -475,11 +654,13 @@ jobs: # --------------------------------------------------------------------------- crates-publish: name: Publish crates - needs: build + needs: [prepare, build] + if: needs.prepare.outputs.release_tag != '' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: + ref: ${{ needs.prepare.outputs.source_sha }} submodules: true - uses: dtolnay/rust-toolchain@stable @@ -509,15 +690,20 @@ jobs: # --------------------------------------------------------------------------- pypi-publish: name: Publish Python SDK - needs: build + needs: [prepare, build] + if: needs.prepare.outputs.release_tag != '' runs-on: ubuntu-latest permissions: + actions: read + contents: read id-token: write environment: name: pypi url: https://pypi.org/p/microsandbox steps: - uses: actions/checkout@v4 + with: + ref: ${{ needs.prepare.outputs.source_sha }} - uses: astral-sh/setup-uv@v3 From e662a14e60e072d10f709889ee7d7633d123663a Mon Sep 17 00:00:00 2001 From: Leavi15 Date: Thu, 16 Apr 2026 15:30:52 -0600 Subject: [PATCH 07/25] chore(docs): add detailed instructions for APT-managed CLI installation --- README.md | 10 ++++++++++ docs/cli/overview.mdx | 15 +++++++++++++-- docs/getting-started/quickstart.mdx | 12 +++++++++++- 3 files changed, 34 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 5ed0f144..11ff6dd9 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,16 @@ > Or install the `msb` command globally: > > ```sh +> # Debian / Ubuntu +> curl -fsSL https://apt.microsandbox.dev/microsandbox-archive-keyring.gpg \ +> | sudo tee /usr/share/keyrings/microsandbox-archive-keyring.gpg >/dev/null +> echo "deb [signed-by=/usr/share/keyrings/microsandbox-archive-keyring.gpg] https://apt.microsandbox.dev stable main" \ +> | sudo tee /etc/apt/sources.list.d/microsandbox.list >/dev/null +> sudo apt update +> sudo apt install microsandbox +> ``` +> ```sh +> # Fallback installer (other Linux / macOS) > curl -fsSL https://install.microsandbox.dev | sh > ``` > diff --git a/docs/cli/overview.mdx b/docs/cli/overview.mdx index ce7ff125..524a09ca 100644 --- a/docs/cli/overview.mdx +++ b/docs/cli/overview.mdx @@ -9,6 +9,17 @@ The `msb` CLI lets you create, manage, and interact with sandboxes from the term ## Install ```bash +# Debian / Ubuntu +curl -fsSL https://apt.microsandbox.dev/microsandbox-archive-keyring.gpg \ + | sudo tee /usr/share/keyrings/microsandbox-archive-keyring.gpg >/dev/null +echo "deb [signed-by=/usr/share/keyrings/microsandbox-archive-keyring.gpg] https://apt.microsandbox.dev stable main" \ + | sudo tee /etc/apt/sources.list.d/microsandbox.list >/dev/null +sudo apt update +sudo apt install microsandbox +``` + +```bash +# Fallback installer (other Linux / macOS) curl -fsSL https://install.microsandbox.dev | sh ``` @@ -59,8 +70,8 @@ msb install ubuntu # Install as 'ubuntu' command msb uninstall ubuntu # Remove installed command # Self-management -msb self update # Update msb to latest -msb self uninstall # Remove msb +msb self update # Update msb to latest (APT installs redirect to apt upgrade) +msb self uninstall # Remove msb (APT installs redirect to apt remove) ``` diff --git a/docs/getting-started/quickstart.mdx b/docs/getting-started/quickstart.mdx index d3540163..7d8eddf3 100644 --- a/docs/getting-started/quickstart.mdx +++ b/docs/getting-started/quickstart.mdx @@ -34,10 +34,20 @@ icon: "bolt" ``` ```bash CLI - curl -fsSL https://install.microsandbox.dev | sh + curl -fsSL https://apt.microsandbox.dev/microsandbox-archive-keyring.gpg \ + | sudo tee /usr/share/keyrings/microsandbox-archive-keyring.gpg >/dev/null + echo "deb [signed-by=/usr/share/keyrings/microsandbox-archive-keyring.gpg] https://apt.microsandbox.dev stable main" \ + | sudo tee /etc/apt/sources.list.d/microsandbox.list >/dev/null + sudo apt update + sudo apt install microsandbox ``` + + + On other Linux distributions and on macOS, use the fallback installer: + `curl -fsSL https://install.microsandbox.dev | sh` + From 258a43cd6cd42a25e593842a2551885b2b43b189 Mon Sep 17 00:00:00 2001 From: Leavi15 Date: Thu, 16 Apr 2026 16:27:27 -0600 Subject: [PATCH 08/25] feat(build): add package defaults and libkrunfw build tool checks --- justfile | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/justfile b/justfile index ff751143..2d72b755 100644 --- a/justfile +++ b/justfile @@ -1,6 +1,9 @@ # Version constants for libkrunfw. Keep in sync with microsandbox-utils/lib/lib.rs. LIBKRUNFW_ABI := "5" LIBKRUNFW_VERSION := "5.2.1" +DEFAULT_PACKAGE_FORMAT := "deb" +LOCAL_PACKAGE_DIST_DIR := "dist/packages" +LINUX_PACKAGE_NAME := "microsandbox" # Set up the development environment, build, and install. Prerequisites: just, git (+ Docker on macOS). setup: _install-dev-deps @@ -127,9 +130,35 @@ _ensure-libkrunfw: # Build libkrunfw on Linux. Requires: kernel build dependencies (gcc, make, flex, bison, etc.). [linux] -build-libkrunfw: +_require-libkrunfw-build-tools: #!/usr/bin/env bash set -euo pipefail + missing=() + + for cmd in bc bison flex gcc make python3; do + if ! command -v "$cmd" >/dev/null 2>&1; then + missing+=("$cmd") + fi + done + + if [ "${#missing[@]}" -gt 0 ]; then + echo "error: missing build tools for libkrunfw: ${missing[*]}" >&2 + echo "hint: install them with: sudo apt-get install -y bc bison flex gcc make libelf-dev python3-pyelftools libcap-ng-dev" >&2 + exit 1 + fi + +[linux] +build-libkrunfw: _require-libkrunfw-build-tools + #!/usr/bin/env bash + set -euo pipefail + kernel_version="$(sed -n 's/^KERNEL_VERSION = //p' vendor/libkrunfw/Makefile | head -n1)" + kernel_tarball="vendor/libkrunfw/tarballs/${kernel_version}.tar.xz" + + if [ -f "$kernel_tarball" ] && ! tar -tf "$kernel_tarball" >/dev/null 2>&1; then + echo "Removing corrupt kernel tarball: $kernel_tarball" + rm -f "$kernel_tarball" + fi + cd vendor/libkrunfw make -j$(nproc) cd ../.. From 4747013056e28e0efe3bcb96b224b5093d5e1724 Mon Sep 17 00:00:00 2001 From: Leavi15 Date: Thu, 16 Apr 2026 16:27:34 -0600 Subject: [PATCH 09/25] feat(build): add tasks for local Linux package build, install, and uninstall --- justfile | 106 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) diff --git a/justfile b/justfile index 2d72b755..d97ef596 100644 --- a/justfile +++ b/justfile @@ -261,6 +261,112 @@ uninstall: rm -f ~/.microsandbox/lib/libkrunfw* echo "Removed msb and libkrunfw from ~/.microsandbox/" +# Build a local Linux package from the current build outputs. +[linux] +package-local format=DEFAULT_PACKAGE_FORMAT revision="1": (build-msb "release") build-libkrunfw + #!/usr/bin/env bash + set -euo pipefail + + arch="$(uname -m)" + package_format="{{ format }}" + revision="{{ revision }}" + version="$(sed -n 's/^version = "\(.*\)"$/\1/p' Cargo.toml | head -n1)" + output_dir="{{ LOCAL_PACKAGE_DIST_DIR }}/$package_format" + + test -n "$version" || { echo "error: could not determine workspace version from Cargo.toml"; exit 1; } + + case "$package_format" in + deb) + echo "==> Packaging {{ LINUX_PACKAGE_NAME }} $version-$revision as $package_format..." + bash scripts/package-deb.sh \ + --arch "$arch" \ + --version "$version" \ + --revision "$revision" \ + --msb "build/msb" \ + --libkrunfw "build/libkrunfw.so.{{ LIBKRUNFW_VERSION }}" \ + --output-dir "$output_dir" + ;; + *) + echo "error: unsupported local package format: $package_format" >&2 + echo "supported formats: deb" >&2 + exit 1 + ;; + esac + +# Build the local Linux package and install it with the matching system tool. +[linux] +install-package-local format=DEFAULT_PACKAGE_FORMAT revision="1": (package-local format revision) + #!/usr/bin/env bash + set -euo pipefail + + package_format="{{ format }}" + revision="{{ revision }}" + version="$(sed -n 's/^version = "\(.*\)"$/\1/p' Cargo.toml | head -n1)" + test -n "$version" || { echo "error: could not determine workspace version from Cargo.toml"; exit 1; } + + case "$package_format" in + deb) + case "$(uname -m)" in + x86_64) + package_arch="amd64" + ;; + aarch64) + package_arch="arm64" + ;; + *) + echo "error: unsupported Debian architecture: $(uname -m)" >&2 + exit 1 + ;; + esac + package_path="$PWD/{{ LOCAL_PACKAGE_DIST_DIR }}/$package_format/{{ LINUX_PACKAGE_NAME }}_${version}-${revision}_${package_arch}.deb" + ;; + *) + echo "error: unsupported local package format: $package_format" >&2 + echo "supported formats: deb" >&2 + exit 1 + ;; + esac + + test -f "$package_path" || { echo "error: package not found: $package_path"; exit 1; } + stage_dir="$(mktemp -d /tmp/microsandbox-package.XXXXXX)" + stage_package="$stage_dir/$(basename "$package_path")" + trap 'rm -rf "$stage_dir"' EXIT + + chmod 755 "$stage_dir" + install -m644 "$package_path" "$stage_package" + + case "$package_format" in + deb) + echo "==> Installing $stage_package..." + sudo apt-get update + sudo apt install --reinstall -y "$stage_package" + ;; + esac + +# Remove the locally installed Linux package with the matching system tool. +[linux] +uninstall-package-local format=DEFAULT_PACKAGE_FORMAT: + #!/usr/bin/env bash + set -euo pipefail + + package_format="{{ format }}" + + case "$package_format" in + deb) + if dpkg-query -W -f='${Status}' "{{ LINUX_PACKAGE_NAME }}" 2>/dev/null | grep -q "install ok installed"; then + echo "==> Removing {{ LINUX_PACKAGE_NAME }}..." + sudo apt remove -y "{{ LINUX_PACKAGE_NAME }}" + else + echo "{{ LINUX_PACKAGE_NAME }} is not installed." + fi + ;; + *) + echo "error: unsupported local package format: $package_format" >&2 + echo "supported formats: deb" >&2 + exit 1 + ;; + esac + # Clean build artifacts. clean: rm -rf build From dc19963cc73c4a2e653a1d7ebb0869983e62c139 Mon Sep 17 00:00:00 2001 From: Leavi15 Date: Thu, 16 Apr 2026 16:34:32 -0600 Subject: [PATCH 10/25] feat(scripts): introduce scripts for building APT repo and baseline artifacts --- scripts/build-apt-baseline-artifacts.sh | 126 ++++++++++++++++ scripts/build-apt-repo.sh | 184 ++++++++++++++++++++++++ 2 files changed, 310 insertions(+) create mode 100644 scripts/build-apt-baseline-artifacts.sh create mode 100755 scripts/build-apt-repo.sh diff --git a/scripts/build-apt-baseline-artifacts.sh b/scripts/build-apt-baseline-artifacts.sh new file mode 100644 index 00000000..ff2dfead --- /dev/null +++ b/scripts/build-apt-baseline-artifacts.sh @@ -0,0 +1,126 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + cat <<'EOF' +Usage: scripts/build-apt-baseline-artifacts.sh --output-dir [--image ] + +Build the Linux artifacts used for APT packaging on an older Debian baseline so +the resulting package remains compatible with the supported Debian/Ubuntu matrix. +EOF +} + +require_cmd() { + command -v "$1" >/dev/null 2>&1 || { + echo "error: required command not found: $1" >&2 + exit 1 + } +} + +OUTPUT_DIR="" +BASELINE_IMAGE="${APT_BASELINE_IMAGE:-docker.io/library/rust:1-bullseye}" +CONTAINER_RUNTIME="${CONTAINER_RUNTIME:-docker}" +LIBKRUNFW_VERSION="${LIBKRUNFW_VERSION:-5.2.1}" + +while [[ $# -gt 0 ]]; do + case "$1" in + --output-dir) + OUTPUT_DIR="$2" + shift 2 + ;; + --image) + BASELINE_IMAGE="$2" + shift 2 + ;; + -h | --help) + usage + exit 0 + ;; + *) + echo "error: unknown argument: $1" >&2 + usage >&2 + exit 1 + ;; + esac +done + +[[ -n "$OUTPUT_DIR" ]] || { + usage >&2 + exit 1 +} + +require_cmd "$CONTAINER_RUNTIME" + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +OUTPUT_DIR_ABS="$(mkdir -p "$OUTPUT_DIR" && cd "$OUTPUT_DIR" && pwd)" +HOST_UID="$(id -u)" +HOST_GID="$(id -g)" + +"$CONTAINER_RUNTIME" run --rm \ + -e DEBIAN_FRONTEND=noninteractive \ + -e HOST_UID="$HOST_UID" \ + -e HOST_GID="$HOST_GID" \ + -e LIBKRUNFW_VERSION="$LIBKRUNFW_VERSION" \ + -e OUTPUT_DIR_ABS="$OUTPUT_DIR_ABS" \ + -v "$REPO_ROOT":/workspace \ + -w /workspace \ + "$BASELINE_IMAGE" \ + bash -euo pipefail -c ' + export PATH=/usr/local/cargo/bin:$PATH + + apt-get update + apt-get install -y \ + bc \ + ca-certificates \ + flex \ + bison \ + gcc \ + libcap-ng-dev \ + libdbus-1-dev \ + libelf-dev \ + make \ + musl-tools \ + pkg-config \ + python3-pyelftools + + build_root="$(mktemp -d)" + trap '\''rm -rf "$build_root"'\'' EXIT + + export CARGO_TARGET_DIR="$build_root/target" + + case "$(uname -m)" in + x86_64) + agentd_target="x86_64-unknown-linux-musl" + ;; + aarch64) + agentd_target="aarch64-unknown-linux-musl" + ;; + *) + echo "error: unsupported architecture for agentd build: $(uname -m)" >&2 + exit 1 + ;; + esac + + libkrunfw_root="$build_root/libkrunfw" + cp -a /workspace/vendor/libkrunfw/. "$libkrunfw_root/" + + rustup target add "$agentd_target" + mkdir -p /workspace/build + cargo build --release --manifest-path crates/agentd/Cargo.toml --target "$agentd_target" + install -m755 \ + "$CARGO_TARGET_DIR/$agentd_target/release/agentd" \ + /workspace/build/agentd + + cargo build --release --no-default-features --features net -p microsandbox-cli + make -C "$libkrunfw_root" -j"$(nproc)" + + install -d "$OUTPUT_DIR_ABS" + install -m755 "$CARGO_TARGET_DIR/release/msb" "$OUTPUT_DIR_ABS/msb" + install -m644 \ + "$libkrunfw_root/libkrunfw.so.${LIBKRUNFW_VERSION}" \ + "$OUTPUT_DIR_ABS/libkrunfw.so.${LIBKRUNFW_VERSION}" + + chown -R "$HOST_UID:$HOST_GID" /workspace/build "$OUTPUT_DIR_ABS" + ' + +printf '%s\n' "$OUTPUT_DIR_ABS" diff --git a/scripts/build-apt-repo.sh b/scripts/build-apt-repo.sh new file mode 100755 index 00000000..61c86a43 --- /dev/null +++ b/scripts/build-apt-repo.sh @@ -0,0 +1,184 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + cat <<'EOF' +Usage: scripts/build-apt-repo.sh --input-dir --output-dir --gpg-key-id + +Build and sign a static APT repository from one or more `.deb` files. +EOF +} + +require_cmd() { + command -v "$1" >/dev/null 2>&1 || { + echo "error: required command not found: $1" >&2 + exit 1 + } +} + +render_template() { + local template="$1" + shift + + local rendered + rendered="$(<"$template")" + + while [[ $# -gt 0 ]]; do + local key="$1" + local value="$2" + rendered="${rendered//${key}/${value}}" + shift 2 + done + + printf '%s' "$rendered" +} + +INPUT_DIR="" +OUTPUT_DIR="" +GPG_KEY_ID="" +DISTRIBUTION="stable" +SUITE="stable" +CODENAME="stable" +COMPONENT="main" +ORIGIN="microsandbox" +LABEL="microsandbox" +DESCRIPTION="Microsandbox APT repository" + +while [[ $# -gt 0 ]]; do + case "$1" in + --input-dir) + INPUT_DIR="$2" + shift 2 + ;; + --output-dir) + OUTPUT_DIR="$2" + shift 2 + ;; + --gpg-key-id) + GPG_KEY_ID="$2" + shift 2 + ;; + --distribution) + DISTRIBUTION="$2" + shift 2 + ;; + --suite) + SUITE="$2" + shift 2 + ;; + --codename) + CODENAME="$2" + shift 2 + ;; + --component) + COMPONENT="$2" + shift 2 + ;; + --origin) + ORIGIN="$2" + shift 2 + ;; + --label) + LABEL="$2" + shift 2 + ;; + --description) + DESCRIPTION="$2" + shift 2 + ;; + -h | --help) + usage + exit 0 + ;; + *) + echo "error: unknown argument: $1" >&2 + usage >&2 + exit 1 + ;; + esac +done + +[[ -n "$INPUT_DIR" && -n "$OUTPUT_DIR" && -n "$GPG_KEY_ID" ]] || { + usage >&2 + exit 1 +} + +require_cmd apt-ftparchive +require_cmd dpkg-deb +require_cmd dpkg-scanpackages +require_cmd gpg +require_cmd gzip + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +TEMPLATE_DIR="$REPO_ROOT/packaging/apt" +SOURCE_DATE_EPOCH="${SOURCE_DATE_EPOCH:-$(git -C "$REPO_ROOT" log -1 --format=%ct 2>/dev/null || date +%s)}" + +rm -rf "$OUTPUT_DIR" +mkdir -p "$OUTPUT_DIR" + +POOL_DIR="$OUTPUT_DIR/pool/$COMPONENT/m/microsandbox" +DIST_DIR="$OUTPUT_DIR/dists/$DISTRIBUTION/$COMPONENT" +mkdir -p "$POOL_DIR" "$DIST_DIR" + +mapfile -t DEBS < <(find "$INPUT_DIR" -maxdepth 1 -type f -name '*.deb' | sort) +[[ ${#DEBS[@]} -gt 0 ]] || { + echo "error: no deb packages found in $INPUT_DIR" >&2 + exit 1 +} + +declare -A ARCH_SEEN=() +for deb in "${DEBS[@]}"; do + cp "$deb" "$POOL_DIR/" + arch="$(dpkg-deb -f "$deb" Architecture)" + ARCH_SEEN["$arch"]=1 +done + +mapfile -t ARCHITECTURES < <(printf '%s\n' "${!ARCH_SEEN[@]}" | sort) +ARCHITECTURE_LIST="$(printf '%s ' "${ARCHITECTURES[@]}")" +ARCHITECTURE_LIST="${ARCHITECTURE_LIST% }" + +for arch in "${ARCHITECTURES[@]}"; do + BINARY_DIR="$DIST_DIR/binary-$arch" + mkdir -p "$BINARY_DIR" + ( + cd "$OUTPUT_DIR" + dpkg-scanpackages -a "$arch" "pool/$COMPONENT/m/microsandbox" /dev/null + ) >"$BINARY_DIR/Packages" + gzip -n9 -c "$BINARY_DIR/Packages" >"$BINARY_DIR/Packages.gz" +done + +RELEASE_CONFIG="$OUTPUT_DIR/apt-ftparchive-release.conf" +render_template \ + "$TEMPLATE_DIR/release.conf.template" \ + "@ORIGIN@" "$ORIGIN" \ + "@LABEL@" "$LABEL" \ + "@SUITE@" "$SUITE" \ + "@CODENAME@" "$CODENAME" \ + "@ARCHITECTURES@" "$ARCHITECTURE_LIST" \ + "@COMPONENTS@" "$COMPONENT" \ + "@DESCRIPTION@" "$DESCRIPTION" >"$RELEASE_CONFIG" + +apt-ftparchive -c "$RELEASE_CONFIG" release "$OUTPUT_DIR/dists/$DISTRIBUTION" \ + >"$OUTPUT_DIR/dists/$DISTRIBUTION/Release" + +GPG_COMMON_ARGS=(--batch --yes --pinentry-mode loopback --local-user "$GPG_KEY_ID") +if [[ -n "${APT_GPG_PASSPHRASE:-}" ]]; then + GPG_COMMON_ARGS+=(--passphrase "$APT_GPG_PASSPHRASE") +fi + +gpg "${GPG_COMMON_ARGS[@]}" \ + --output "$OUTPUT_DIR/dists/$DISTRIBUTION/Release.gpg" \ + --detach-sign "$OUTPUT_DIR/dists/$DISTRIBUTION/Release" + +gpg "${GPG_COMMON_ARGS[@]}" \ + --output "$OUTPUT_DIR/dists/$DISTRIBUTION/InRelease" \ + --clearsign "$OUTPUT_DIR/dists/$DISTRIBUTION/Release" + +gpg --batch --yes --output "$OUTPUT_DIR/microsandbox-archive-keyring.gpg" --export "$GPG_KEY_ID" +gpg --batch --yes --armor --output "$OUTPUT_DIR/microsandbox-archive-keyring.asc" --export "$GPG_KEY_ID" + +while IFS= read -r -d '' path; do + touch -h -d "@$SOURCE_DATE_EPOCH" "$path" +done < <(find "$OUTPUT_DIR" -print0) + +printf '%s\n' "$OUTPUT_DIR" From 5ad164097ade075f4498088807efaa79787ebaa6 Mon Sep 17 00:00:00 2001 From: Leavi15 Date: Thu, 16 Apr 2026 16:34:36 -0600 Subject: [PATCH 11/25] feat(scripts): add APT smoke test script for end-to-end CLI validation --- scripts/apt-smoke-test.sh | 100 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100755 scripts/apt-smoke-test.sh diff --git a/scripts/apt-smoke-test.sh b/scripts/apt-smoke-test.sh new file mode 100755 index 00000000..a4695181 --- /dev/null +++ b/scripts/apt-smoke-test.sh @@ -0,0 +1,100 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + cat <<'EOF' +Usage: scripts/apt-smoke-test.sh --repo-url [--keyring-path | --key-url ] + +Install microsandbox from an APT repository and run a short end-to-end CLI smoke +test on a Linux host with KVM. +EOF +} + +require_cmd() { + command -v "$1" >/dev/null 2>&1 || { + echo "error: required command not found: $1" >&2 + exit 1 + } +} + +REPO_URL="" +KEYRING_PATH="" +KEY_URL="" +DISTRIBUTION="stable" +PACKAGE_NAME="microsandbox" +KEYRING_DEST="/usr/share/keyrings/microsandbox-archive-keyring.gpg" +SOURCE_LIST="/etc/apt/sources.list.d/microsandbox.list" +SANDBOX_NAME="apt-smoke" + +while [[ $# -gt 0 ]]; do + case "$1" in + --repo-url) + REPO_URL="$2" + shift 2 + ;; + --keyring-path) + KEYRING_PATH="$2" + shift 2 + ;; + --key-url) + KEY_URL="$2" + shift 2 + ;; + --distribution) + DISTRIBUTION="$2" + shift 2 + ;; + --package) + PACKAGE_NAME="$2" + shift 2 + ;; + -h | --help) + usage + exit 0 + ;; + *) + echo "error: unknown argument: $1" >&2 + usage >&2 + exit 1 + ;; + esac +done + +[[ -n "$REPO_URL" ]] || { + usage >&2 + exit 1 +} + +if [[ -z "$KEYRING_PATH" && -z "$KEY_URL" ]]; then + usage >&2 + exit 1 +fi + +require_cmd sudo +require_cmd apt-get +require_cmd timeout + +cleanup() { + msb stop "$SANDBOX_NAME" >/dev/null 2>&1 || true + msb rm "$SANDBOX_NAME" >/dev/null 2>&1 || true +} +trap cleanup EXIT + +if [[ -n "$KEYRING_PATH" ]]; then + sudo install -Dm644 "$KEYRING_PATH" "$KEYRING_DEST" +else + require_cmd curl + curl -fsSL "$KEY_URL" | sudo tee "$KEYRING_DEST" >/dev/null +fi + +echo "deb [signed-by=$KEYRING_DEST] $REPO_URL $DISTRIBUTION main" | \ + sudo tee "$SOURCE_LIST" >/dev/null + +sudo apt-get update +sudo apt-get install -y "$PACKAGE_NAME" + +timeout 600 msb run --name "$SANDBOX_NAME" alpine -- sh -lc 'echo apt smoke hello' +timeout 600 msb start "$SANDBOX_NAME" +timeout 600 msb exec "$SANDBOX_NAME" -- sh -lc 'uname -a' +timeout 600 msb stop "$SANDBOX_NAME" +timeout 600 msb rm "$SANDBOX_NAME" From e3b9a19bc7ed3f893703caecd088adae3968afa9 Mon Sep 17 00:00:00 2001 From: Leavi15 Date: Thu, 16 Apr 2026 16:34:42 -0600 Subject: [PATCH 12/25] feat(scripts): add utilities for generating and importing APT signing keys --- scripts/generate-apt-test-key.sh | 74 +++++++++++++++++++++++++ scripts/import-apt-signing-key.sh | 89 +++++++++++++++++++++++++++++++ 2 files changed, 163 insertions(+) create mode 100755 scripts/generate-apt-test-key.sh create mode 100755 scripts/import-apt-signing-key.sh diff --git a/scripts/generate-apt-test-key.sh b/scripts/generate-apt-test-key.sh new file mode 100755 index 00000000..e83cdc8a --- /dev/null +++ b/scripts/generate-apt-test-key.sh @@ -0,0 +1,74 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + cat <<'EOF' +Usage: scripts/generate-apt-test-key.sh --gnupg-home + +Generate an ephemeral unprotected signing key for CI/package tests. +EOF +} + +GNUPG_HOME="" +NAME_REAL="Microsandbox Test Repository" +NAME_EMAIL="ci@microsandbox.dev" + +while [[ $# -gt 0 ]]; do + case "$1" in + --gnupg-home) + GNUPG_HOME="$2" + shift 2 + ;; + --name-real) + NAME_REAL="$2" + shift 2 + ;; + --name-email) + NAME_EMAIL="$2" + shift 2 + ;; + -h | --help) + usage + exit 0 + ;; + *) + echo "error: unknown argument: $1" >&2 + usage >&2 + exit 1 + ;; + esac +done + +[[ -n "$GNUPG_HOME" ]] || { + usage >&2 + exit 1 +} + +command -v gpg >/dev/null 2>&1 || { + echo "error: required command not found: gpg" >&2 + exit 1 +} + +mkdir -p "$GNUPG_HOME" +chmod 700 "$GNUPG_HOME" + +cat >"$GNUPG_HOME/key.conf" </dev/null 2>&1 +FINGERPRINT="$( + gpg --homedir "$GNUPG_HOME" --batch --with-colons --list-secret-keys "$NAME_EMAIL" | + awk -F: '$1 == "fpr" { print $10; exit }' +)" + +echo "$FINGERPRINT:6:" | gpg --homedir "$GNUPG_HOME" --batch --import-ownertrust >/dev/null 2>&1 +printf '%s\n' "$FINGERPRINT" diff --git a/scripts/import-apt-signing-key.sh b/scripts/import-apt-signing-key.sh new file mode 100755 index 00000000..daed1a6b --- /dev/null +++ b/scripts/import-apt-signing-key.sh @@ -0,0 +1,89 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + cat <<'EOF' +Usage: scripts/import-apt-signing-key.sh --gnupg-home [--private-key-file ] + +Import the production APT signing key into a dedicated GnuPG home and print the +fingerprint. The private key can come from `--private-key-file`, +`APT_GPG_PRIVATE_KEY`, or `APT_GPG_PRIVATE_KEY_BASE64`. +EOF +} + +GNUPG_HOME="" +PRIVATE_KEY_FILE="" + +while [[ $# -gt 0 ]]; do + case "$1" in + --gnupg-home) + GNUPG_HOME="$2" + shift 2 + ;; + --private-key-file) + PRIVATE_KEY_FILE="$2" + shift 2 + ;; + -h | --help) + usage + exit 0 + ;; + *) + echo "error: unknown argument: $1" >&2 + usage >&2 + exit 1 + ;; + esac +done + +[[ -n "$GNUPG_HOME" ]] || { + usage >&2 + exit 1 +} + +command -v gpg >/dev/null 2>&1 || { + echo "error: required command not found: gpg" >&2 + exit 1 +} + +mkdir -p "$GNUPG_HOME" +chmod 700 "$GNUPG_HOME" + +TEMP_KEY_FILE="" +cleanup() { + if [[ -n "$TEMP_KEY_FILE" && -f "$TEMP_KEY_FILE" ]]; then + rm -f "$TEMP_KEY_FILE" + fi +} +trap cleanup EXIT + +if [[ -z "$PRIVATE_KEY_FILE" ]]; then + if [[ -n "${APT_GPG_PRIVATE_KEY:-}" ]]; then + TEMP_KEY_FILE="$(mktemp)" + printf '%s\n' "$APT_GPG_PRIVATE_KEY" >"$TEMP_KEY_FILE" + PRIVATE_KEY_FILE="$TEMP_KEY_FILE" + elif [[ -n "${APT_GPG_PRIVATE_KEY_BASE64:-}" ]]; then + TEMP_KEY_FILE="$(mktemp)" + printf '%s' "$APT_GPG_PRIVATE_KEY_BASE64" | base64 --decode >"$TEMP_KEY_FILE" + PRIVATE_KEY_FILE="$TEMP_KEY_FILE" + fi +fi + +[[ -n "$PRIVATE_KEY_FILE" && -f "$PRIVATE_KEY_FILE" ]] || { + echo "error: no signing key provided" >&2 + exit 1 +} + +gpg --homedir "$GNUPG_HOME" --batch --import "$PRIVATE_KEY_FILE" >/dev/null 2>&1 +FINGERPRINT="$( + gpg --homedir "$GNUPG_HOME" --batch --with-colons --list-secret-keys | + awk -F: '$1 == "fpr" { print $10; exit }' +)" + +[[ -n "$FINGERPRINT" ]] || { + echo "error: imported key fingerprint could not be determined" >&2 + exit 1 +} + +echo "$FINGERPRINT:6:" | gpg --homedir "$GNUPG_HOME" --batch --import-ownertrust >/dev/null 2>&1 +printf '%s\n' "$FINGERPRINT" From d5d2741d35d14565badd10c12430a2347fc73392 Mon Sep 17 00:00:00 2001 From: Leavi15 Date: Thu, 16 Apr 2026 16:34:45 -0600 Subject: [PATCH 13/25] feat(scripts): add APT repo testing script for validating install, upgrade, and removal --- scripts/test-apt-repo.sh | 180 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100755 scripts/test-apt-repo.sh diff --git a/scripts/test-apt-repo.sh b/scripts/test-apt-repo.sh new file mode 100755 index 00000000..9ca48d8c --- /dev/null +++ b/scripts/test-apt-repo.sh @@ -0,0 +1,180 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + cat <<'EOF' +Usage: scripts/test-apt-repo.sh --repo-v1 --repo-v2 --keyring \ + --bad-keyring [--image ...] + +Validate install, reinstall, upgrade, remove, purge, and signature failures +against a local signed APT repository inside Debian/Ubuntu containers. +EOF +} + +require_cmd() { + command -v "$1" >/dev/null 2>&1 || { + echo "error: required command not found: $1" >&2 + exit 1 + } +} + +canonical_path() { + realpath "$1" +} + +REPO_V1="" +REPO_V2="" +KEYRING="" +BAD_KEYRING="" +IMAGES=() + +while [[ $# -gt 0 ]]; do + case "$1" in + --repo-v1) + REPO_V1="$2" + shift 2 + ;; + --repo-v2) + REPO_V2="$2" + shift 2 + ;; + --keyring) + KEYRING="$2" + shift 2 + ;; + --bad-keyring) + BAD_KEYRING="$2" + shift 2 + ;; + --image) + IMAGES+=("$2") + shift 2 + ;; + -h | --help) + usage + exit 0 + ;; + *) + echo "error: unknown argument: $1" >&2 + usage >&2 + exit 1 + ;; + esac +done + +[[ -n "$REPO_V1" && -n "$REPO_V2" && -n "$KEYRING" && -n "$BAD_KEYRING" ]] || { + usage >&2 + exit 1 +} + +if [[ ${#IMAGES[@]} -eq 0 ]]; then + IMAGES=("debian:12" "ubuntu:22.04" "ubuntu:24.04") +fi + +require_cmd docker +require_cmd dpkg-deb +require_cmd realpath + +[[ -d "$REPO_V1" && -d "$REPO_V2" ]] || { + echo "error: repository directories must exist" >&2 + exit 1 +} +[[ -f "$KEYRING" && -f "$BAD_KEYRING" ]] || { + echo "error: keyring files must exist" >&2 + exit 1 +} + +REPO_V1="$(canonical_path "$REPO_V1")" +REPO_V2="$(canonical_path "$REPO_V2")" +KEYRING="$(canonical_path "$KEYRING")" +BAD_KEYRING="$(canonical_path "$BAD_KEYRING")" + +DEB_V1="$(find "$REPO_V1/pool" -type f -name 'microsandbox_*.deb' | sort | head -n1)" +DEB_V2="$(find "$REPO_V2/pool" -type f -name 'microsandbox_*.deb' | sort | head -n1)" +VERSION_V1="$(dpkg-deb -f "$DEB_V1" Version)" +VERSION_V2="$(dpkg-deb -f "$DEB_V2" Version)" + +for image in "${IMAGES[@]}"; do + echo "==> Testing install and upgrade in $image" + docker run --rm \ + -e DEBIAN_FRONTEND=noninteractive \ + -e VERSION_V1="$VERSION_V1" \ + -e VERSION_V2="$VERSION_V2" \ + -v "$REPO_V1":/repo-v1:ro \ + -v "$REPO_V2":/repo-v2:ro \ + -v "$KEYRING":/tmp/microsandbox-archive-keyring.gpg:ro \ + "$image" \ + bash -euxo pipefail -c ' + apt-get update + apt-get install -y ca-certificates + mkdir -p /root/.microsandbox + touch /root/.microsandbox/state-marker + + install -Dm644 /tmp/microsandbox-archive-keyring.gpg \ + /usr/share/keyrings/microsandbox-archive-keyring.gpg + echo "deb [signed-by=/usr/share/keyrings/microsandbox-archive-keyring.gpg] file:///repo-v1 stable main" \ + >/etc/apt/sources.list.d/microsandbox.list + + apt-get update + apt-get install -y microsandbox + test -x /usr/bin/msb + versioned_lib="$(find /usr/lib/microsandbox -maxdepth 1 -type f -name "libkrunfw.so.*" | sort | head -n1)" + test -n "$versioned_lib" + soname_link="$(basename "$versioned_lib" | sed -E "s/^(libkrunfw\\.so\\.[0-9]+)\\..*$/\\1/")" + test -L "/usr/lib/microsandbox/$soname_link" + test -L /usr/lib/microsandbox/libkrunfw.so + test "$(readlink "/usr/lib/microsandbox/$soname_link")" = "$(basename "$versioned_lib")" + test "$(readlink /usr/lib/microsandbox/libkrunfw.so)" = "$soname_link" + msb --version + test "$(dpkg-query -W -f="\${Version}\n" microsandbox)" = "$VERSION_V1" + + apt-get install -y --reinstall microsandbox + test "$(dpkg-query -W -f="\${Version}\n" microsandbox)" = "$VERSION_V1" + + echo "deb [signed-by=/usr/share/keyrings/microsandbox-archive-keyring.gpg] file:///repo-v2 stable main" \ + >/etc/apt/sources.list.d/microsandbox.list + rm -rf /var/lib/apt/lists/* + apt-get update + apt-get install -y --only-upgrade microsandbox + test "$(dpkg-query -W -f="\${Version}\n" microsandbox)" = "$VERSION_V2" + + apt-get remove -y microsandbox + test ! -e /usr/bin/msb + test -e /root/.microsandbox/state-marker + + apt-get install -y microsandbox + apt-get purge -y microsandbox + test ! -e /usr/bin/msb + test -e /root/.microsandbox/state-marker + ' + + echo "==> Testing missing key failure in $image" + docker run --rm \ + -e DEBIAN_FRONTEND=noninteractive \ + -v "$REPO_V1":/repo-v1:ro \ + "$image" \ + bash -euxo pipefail -c ' + echo "deb file:///repo-v1 stable main" >/etc/apt/sources.list.d/microsandbox.list + if apt-get update; then + echo "error: apt update succeeded without repository key" >&2 + exit 1 + fi + ' + + echo "==> Testing wrong key failure in $image" + docker run --rm \ + -e DEBIAN_FRONTEND=noninteractive \ + -v "$REPO_V1":/repo-v1:ro \ + -v "$BAD_KEYRING":/tmp/wrong-keyring.gpg:ro \ + "$image" \ + bash -euxo pipefail -c ' + install -Dm644 /tmp/wrong-keyring.gpg \ + /usr/share/keyrings/microsandbox-archive-keyring.gpg + echo "deb [signed-by=/usr/share/keyrings/microsandbox-archive-keyring.gpg] file:///repo-v1 stable main" \ + >/etc/apt/sources.list.d/microsandbox.list + if apt-get update; then + echo "error: apt update succeeded with the wrong repository key" >&2 + exit 1 + fi + ' +done From adccf5c01e35b72f760ccff96a9efc1d1048f9f1 Mon Sep 17 00:00:00 2001 From: Leavi15 Date: Thu, 16 Apr 2026 16:34:48 -0600 Subject: [PATCH 14/25] feat(scripts): add validation script for Debian package structural checks and linting --- scripts/validate-deb.sh | 142 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100755 scripts/validate-deb.sh diff --git a/scripts/validate-deb.sh b/scripts/validate-deb.sh new file mode 100755 index 00000000..eaed0eea --- /dev/null +++ b/scripts/validate-deb.sh @@ -0,0 +1,142 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + cat <<'EOF' +Usage: scripts/validate-deb.sh --deb --arch --version + +Run structural validation and lint checks for a microsandbox Debian package. +EOF +} + +require_cmd() { + command -v "$1" >/dev/null 2>&1 || { + echo "error: required command not found: $1" >&2 + exit 1 + } +} + +map_deb_arch() { + case "$1" in + amd64 | x86_64) echo "amd64" ;; + arm64 | aarch64) echo "arm64" ;; + *) + echo "error: unsupported Debian architecture: $1" >&2 + exit 1 + ;; + esac +} + +normalize_version() { + local raw="$1" + local revision="${2:-1}" + local clean="${raw#v}" + if [[ "$clean" == *-* ]]; then + printf '%s\n' "$clean" + else + printf '%s-%s\n' "$clean" "$revision" + fi +} + +DEB_PATH="" +ARCH="" +VERSION="" +REVISION="1" + +while [[ $# -gt 0 ]]; do + case "$1" in + --deb) + DEB_PATH="$2" + shift 2 + ;; + --arch) + ARCH="$2" + shift 2 + ;; + --version) + VERSION="$2" + shift 2 + ;; + --revision) + REVISION="$2" + shift 2 + ;; + -h | --help) + usage + exit 0 + ;; + *) + echo "error: unknown argument: $1" >&2 + usage >&2 + exit 1 + ;; + esac +done + +[[ -n "$DEB_PATH" && -n "$ARCH" && -n "$VERSION" ]] || { + usage >&2 + exit 1 +} + +require_cmd dpkg-deb +require_cmd lintian +require_cmd tar +require_cmd readlink + +[[ -f "$DEB_PATH" ]] || { + echo "error: package not found: $DEB_PATH" >&2 + exit 1 +} + +EXPECTED_ARCH="$(map_deb_arch "$ARCH")" +EXPECTED_VERSION="$(normalize_version "$VERSION" "$REVISION")" + +[[ "$(dpkg-deb -f "$DEB_PATH" Package)" == "microsandbox" ]] +[[ "$(dpkg-deb -f "$DEB_PATH" Architecture)" == "$EXPECTED_ARCH" ]] +[[ "$(dpkg-deb -f "$DEB_PATH" Version)" == "$EXPECTED_VERSION" ]] + +TMP_DIR="$(mktemp -d)" +trap 'rm -rf "$TMP_DIR"' EXIT + +dpkg-deb -x "$DEB_PATH" "$TMP_DIR/root" + +[[ -x "$TMP_DIR/root/usr/bin/msb" ]] +[[ -d "$TMP_DIR/root/usr/lib/microsandbox" ]] + +mapfile -t VERSIONED_LIBS < <(find "$TMP_DIR/root/usr/lib/microsandbox" \ + -maxdepth 1 -type f -name 'libkrunfw.so.*' | sort) +[[ ${#VERSIONED_LIBS[@]} -eq 1 ]] + +VERSIONED_LIB_BASENAME="$(basename "${VERSIONED_LIBS[0]}")" +if [[ "$VERSIONED_LIB_BASENAME" =~ ^libkrunfw\.so\.([0-9]+)\..+$ ]]; then + LIBKRUNFW_SONAME_LINK="libkrunfw.so.${BASH_REMATCH[1]}" +else + echo "error: unsupported libkrunfw filename in package: $VERSIONED_LIB_BASENAME" >&2 + exit 1 +fi + +[[ -L "$TMP_DIR/root/usr/lib/microsandbox/$LIBKRUNFW_SONAME_LINK" ]] +[[ -L "$TMP_DIR/root/usr/lib/microsandbox/libkrunfw.so" ]] +[[ "$(readlink "$TMP_DIR/root/usr/lib/microsandbox/$LIBKRUNFW_SONAME_LINK")" == "$VERSIONED_LIB_BASENAME" ]] +[[ "$(readlink "$TMP_DIR/root/usr/lib/microsandbox/libkrunfw.so")" == "$LIBKRUNFW_SONAME_LINK" ]] + +[[ -f "$TMP_DIR/root/usr/share/doc/microsandbox/copyright" ]] +[[ -f "$TMP_DIR/root/usr/share/doc/microsandbox/changelog.Debian.gz" ]] + +[[ ! -e "$TMP_DIR/root/root/.microsandbox" ]] +[[ ! -e "$TMP_DIR/root/etc/profile.d/microsandbox.sh" ]] +[[ ! -e "$TMP_DIR/root/etc/bash.bashrc" ]] +[[ ! -e "$TMP_DIR/root/etc/skel/.bashrc" ]] +[[ ! -e "$TMP_DIR/root/usr/share/fish/vendor_conf.d/microsandbox.fish" ]] + +CONTROL_LIST="$(dpkg-deb --ctrl-tarfile "$DEB_PATH" | tar -tf -)" +for forbidden in preinst postinst prerm postrm triggers; do + if grep -Eq "(^|/)$forbidden$" <<<"$CONTROL_LIST"; then + echo "error: unexpected maintainer script present: $forbidden" >&2 + exit 1 + fi +done + +dpkg-deb --info "$DEB_PATH" >/dev/null +dpkg-deb --contents "$DEB_PATH" >/dev/null +lintian --fail-on error "$DEB_PATH" From e39aab6492d99c3706bde9f25d076fb0f654fea5 Mon Sep 17 00:00:00 2001 From: Leavi15 Date: Thu, 16 Apr 2026 16:34:52 -0600 Subject: [PATCH 15/25] feat(scripts): add script to build Debian package for microsandbox CLI and runtime --- scripts/package-deb.sh | 232 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 232 insertions(+) create mode 100755 scripts/package-deb.sh diff --git a/scripts/package-deb.sh b/scripts/package-deb.sh new file mode 100755 index 00000000..40bdb38f --- /dev/null +++ b/scripts/package-deb.sh @@ -0,0 +1,232 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + cat <<'EOF' +Usage: scripts/package-deb.sh --arch --version --msb \ + --libkrunfw --output-dir [--package ] [--revision ] + +Build a Debian package for microsandbox from Linux release artifacts. +EOF +} + +require_cmd() { + command -v "$1" >/dev/null 2>&1 || { + echo "error: required command not found: $1" >&2 + exit 1 + } +} + +map_deb_arch() { + case "$1" in + amd64 | x86_64) echo "amd64" ;; + arm64 | aarch64) echo "arm64" ;; + *) + echo "error: unsupported Debian architecture: $1" >&2 + exit 1 + ;; + esac +} + +normalize_version() { + local raw="$1" + local revision="$2" + local clean="${raw#v}" + + if [[ "$clean" == *-* ]]; then + printf '%s\n' "$clean" + return + fi + + printf '%s-%s\n' "$clean" "$revision" +} + +render_template() { + local template="$1" + shift + + local rendered + rendered="$(<"$template")" + + while [[ $# -gt 0 ]]; do + local key="$1" + local value="$2" + rendered="${rendered//${key}/${value}}" + shift 2 + done + + printf '%s' "$rendered" +} + +PACKAGE_NAME="microsandbox" +ARCH="" +VERSION="" +REVISION="1" +MSB_PATH="" +LIBKRUNFW_PATH="" +OUTPUT_DIR="" + +while [[ $# -gt 0 ]]; do + case "$1" in + --arch) + ARCH="$2" + shift 2 + ;; + --version) + VERSION="$2" + shift 2 + ;; + --revision) + REVISION="$2" + shift 2 + ;; + --msb) + MSB_PATH="$2" + shift 2 + ;; + --libkrunfw) + LIBKRUNFW_PATH="$2" + shift 2 + ;; + --output-dir) + OUTPUT_DIR="$2" + shift 2 + ;; + --package) + PACKAGE_NAME="$2" + shift 2 + ;; + -h | --help) + usage + exit 0 + ;; + *) + echo "error: unknown argument: $1" >&2 + usage >&2 + exit 1 + ;; + esac +done + +[[ -n "$ARCH" && -n "$VERSION" && -n "$MSB_PATH" && -n "$LIBKRUNFW_PATH" && -n "$OUTPUT_DIR" ]] || { + usage >&2 + exit 1 +} + +require_cmd dpkg-deb +require_cmd dpkg-shlibdeps +require_cmd gzip +require_cmd sed + +[[ -f "$MSB_PATH" ]] || { + echo "error: msb binary not found: $MSB_PATH" >&2 + exit 1 +} +[[ -f "$LIBKRUNFW_PATH" ]] || { + echo "error: libkrunfw library not found: $LIBKRUNFW_PATH" >&2 + exit 1 +} + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +TEMPLATE_DIR="$REPO_ROOT/packaging/apt" +DEB_ARCH="$(map_deb_arch "$ARCH")" +DEB_VERSION="$(normalize_version "$VERSION" "$REVISION")" +LIBKRUNFW_BASENAME="$(basename "$LIBKRUNFW_PATH")" +if [[ "$LIBKRUNFW_BASENAME" =~ ^libkrunfw\.so\.([0-9]+)\..+$ ]]; then + LIBKRUNFW_ABI="${BASH_REMATCH[1]}" +else + echo "error: unsupported libkrunfw filename: $LIBKRUNFW_BASENAME" >&2 + exit 1 +fi +LIBKRUNFW_SONAME_LINK="libkrunfw.so.$LIBKRUNFW_ABI" +SOURCE_DATE_EPOCH="${SOURCE_DATE_EPOCH:-$(git -C "$REPO_ROOT" log -1 --format=%ct 2>/dev/null || date +%s)}" + +WORK_DIR="$(mktemp -d)" +trap 'rm -rf "$WORK_DIR"' EXIT + +PACKAGE_ROOT="$WORK_DIR/package" +DEBIAN_DIR="$PACKAGE_ROOT/DEBIAN" +DEBIAN_HELPER_DIR="$WORK_DIR/debian" +DOC_DIR="$PACKAGE_ROOT/usr/share/doc/$PACKAGE_NAME" +BIN_DIR="$PACKAGE_ROOT/usr/bin" +LIB_DIR="$PACKAGE_ROOT/usr/lib/microsandbox" + +mkdir -p "$DEBIAN_DIR" "$DEBIAN_HELPER_DIR" "$DOC_DIR" "$BIN_DIR" "$LIB_DIR" + +install -m755 "$MSB_PATH" "$BIN_DIR/msb" +install -m644 "$LIBKRUNFW_PATH" "$LIB_DIR/$LIBKRUNFW_BASENAME" +ln -s "$LIBKRUNFW_BASENAME" "$LIB_DIR/$LIBKRUNFW_SONAME_LINK" +ln -s "$LIBKRUNFW_SONAME_LINK" "$LIB_DIR/libkrunfw.so" + +install -m644 "$TEMPLATE_DIR/copyright" "$DOC_DIR/copyright" + +CHANGELOG_DATE="$(date -Ru -d "@$SOURCE_DATE_EPOCH")" +cat >"$DOC_DIR/changelog.Debian" < $CHANGELOG_DATE +EOF +gzip -n9 "$DOC_DIR/changelog.Debian" +chmod 644 "$DOC_DIR/changelog.Debian.gz" +find "$PACKAGE_ROOT" -type d -exec chmod 755 {} + + +while IFS= read -r -d '' path; do + touch -h -d "@$SOURCE_DATE_EPOCH" "$path" +done < <(find "$PACKAGE_ROOT" -print0) + +cat >"$DEBIAN_HELPER_DIR/control" < +Standards-Version: 4.7.0 + +Package: $PACKAGE_NAME +Architecture: $DEB_ARCH +Description: Lightweight microVM sandbox CLI + Microsandbox spins up lightweight, hardware-isolated microVMs from a local + CLI. This package installs the \`msb\` command and the private \`libkrunfw\` + runtime library used to boot and manage microsandbox environments on Debian + and Ubuntu systems. +EOF + +cat >"$DEBIAN_DIR/shlibs" <&2 + exit 1 +} + +rm -f "$DEBIAN_DIR/shlibs" + +INSTALLED_SIZE="$(du -sk "$PACKAGE_ROOT" | cut -f1)" + +render_template \ + "$TEMPLATE_DIR/control.template" \ + "@PACKAGE@" "$PACKAGE_NAME" \ + "@VERSION@" "$DEB_VERSION" \ + "@ARCH@" "$DEB_ARCH" \ + "@INSTALLED_SIZE@" "$INSTALLED_SIZE" \ + "@DEPENDS@" "$SHLIBS_DEPENDS" >"$DEBIAN_DIR/control" +printf '\n' >>"$DEBIAN_DIR/control" + +mkdir -p "$OUTPUT_DIR" +OUTPUT_PATH="$OUTPUT_DIR/${PACKAGE_NAME}_${DEB_VERSION}_${DEB_ARCH}.deb" +dpkg-deb --root-owner-group --uniform-compression -Zxz --build "$PACKAGE_ROOT" "$OUTPUT_PATH" >/dev/null + +printf '%s\n' "$OUTPUT_PATH" From b99573d27076f335bb9fe916637017e7e0f65ea9 Mon Sep 17 00:00:00 2001 From: Leavi15 Date: Thu, 16 Apr 2026 16:35:31 -0600 Subject: [PATCH 16/25] feat(packaging): add APT packaging metadata and documentation --- packaging/apt/README.md | 24 ++++++++++++++++++++++++ packaging/apt/control.template | 14 ++++++++++++++ packaging/apt/copyright | 12 ++++++++++++ packaging/apt/release.conf.template | 7 +++++++ 4 files changed, 57 insertions(+) create mode 100644 packaging/apt/README.md create mode 100644 packaging/apt/control.template create mode 100644 packaging/apt/copyright create mode 100644 packaging/apt/release.conf.template diff --git a/packaging/apt/README.md b/packaging/apt/README.md new file mode 100644 index 00000000..0780ba54 --- /dev/null +++ b/packaging/apt/README.md @@ -0,0 +1,24 @@ +# APT Packaging + +This directory contains the Debian/Ubuntu packaging metadata used to build the +`microsandbox` APT package and the signed repository published at +`https://apt.microsandbox.dev`. + +## Contents + +- `control.template`: package control metadata rendered by `scripts/package-deb.sh` +- `release.conf.template`: `apt-ftparchive` release metadata rendered by + `scripts/build-apt-repo.sh` +- `copyright`: Debian machine-readable copyright file installed into the package + +## Local flow + +1. Build baseline-compatible Linux artifacts with + `scripts/build-apt-baseline-artifacts.sh --output-dir build/apt/` +2. Create `.deb` packages with `scripts/package-deb.sh` +3. Generate a signing key with `scripts/generate-apt-test-key.sh` or import the + production key with `scripts/import-apt-signing-key.sh` +4. Build the signed repository with `scripts/build-apt-repo.sh` + +The CI workflows use the same scripts for PR validation, release publication, +and canary smoke tests. diff --git a/packaging/apt/control.template b/packaging/apt/control.template new file mode 100644 index 00000000..4fa89638 --- /dev/null +++ b/packaging/apt/control.template @@ -0,0 +1,14 @@ +Package: @PACKAGE@ +Version: @VERSION@ +Section: utils +Priority: optional +Architecture: @ARCH@ +Installed-Size: @INSTALLED_SIZE@ +Maintainer: Super Rad Company +Depends: @DEPENDS@ +Homepage: https://github.com/superradcompany/microsandbox +Description: Lightweight microVM sandbox CLI + Microsandbox spins up lightweight, hardware-isolated microVMs from a local + CLI. This package installs the `msb` command and the private `libkrunfw` + runtime library used to boot and manage microsandbox environments on Debian + and Ubuntu systems. diff --git a/packaging/apt/copyright b/packaging/apt/copyright new file mode 100644 index 00000000..6ffa24f8 --- /dev/null +++ b/packaging/apt/copyright @@ -0,0 +1,12 @@ +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: microsandbox +Upstream-Contact: Super Rad Company +Source: https://github.com/superradcompany/microsandbox + +Files: * +Copyright: 2025 Super Rad Company +License: Apache-2.0 + +License: Apache-2.0 + On Debian systems, the complete text of the Apache License Version 2.0 + can be found in "/usr/share/common-licenses/Apache-2.0". diff --git a/packaging/apt/release.conf.template b/packaging/apt/release.conf.template new file mode 100644 index 00000000..6a7ea335 --- /dev/null +++ b/packaging/apt/release.conf.template @@ -0,0 +1,7 @@ +APT::FTPArchive::Release::Origin "@ORIGIN@"; +APT::FTPArchive::Release::Label "@LABEL@"; +APT::FTPArchive::Release::Suite "@SUITE@"; +APT::FTPArchive::Release::Codename "@CODENAME@"; +APT::FTPArchive::Release::Architectures "@ARCHITECTURES@"; +APT::FTPArchive::Release::Components "@COMPONENTS@"; +APT::FTPArchive::Release::Description "@DESCRIPTION@"; From 272371ff0d5efc7c04ecf828c4f6f5d486dd23fc Mon Sep 17 00:00:00 2001 From: Leavi15 Date: Sat, 18 Apr 2026 18:24:07 -0600 Subject: [PATCH 17/25] feat(pre-commit): add shellcheck, actionlint, and regression test hooks --- .pre-commit-config.yaml | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 82c436fa..5d0d3dd9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,8 +18,33 @@ repos: - id: no-commit-to-branch args: [--branch, main] + - repo: https://github.com/koalaman/shellcheck-precommit + rev: v0.11.0 + hooks: + - id: shellcheck + args: [-x] + files: ^scripts/.*\.sh$ + + - repo: https://github.com/rhysd/actionlint + rev: v1.7.12 + hooks: + - id: actionlint + - repo: local hooks: + - id: apt-common-regression + name: apt common regression + entry: bash scripts/tests/test-apt-common.sh + language: system + pass_filenames: false + files: ^scripts/(lib/apt-common\.sh|tests/test-apt-common\.sh|.*\.sh)$ + - id: workflow-regressions + name: workflow regression checks + entry: python scripts/tests/test-workflows.py + language: python + additional_dependencies: [PyYAML==6.0.2] + pass_filenames: false + files: ^(\.github/workflows/.*\.ya?ml|scripts/tests/test-workflows\.py)$ - id: cargo-fmt-workspace name: cargo fmt (workspace) entry: cargo fmt --all -- --check From 040264c00a34aae21b5506077899fb0148a3094e Mon Sep 17 00:00:00 2001 From: Leavi15 Date: Sat, 18 Apr 2026 18:24:15 -0600 Subject: [PATCH 18/25] feat(build): add linting tasks and baseline APT artifact support for packaging --- justfile | 37 +++++++++++++++++++++++++++++++++---- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/justfile b/justfile index d97ef596..ba0a30f0 100644 --- a/justfile +++ b/justfile @@ -261,20 +261,49 @@ uninstall: rm -f ~/.microsandbox/lib/libkrunfw* echo "Removed msb and libkrunfw from ~/.microsandbox/" -# Build a local Linux package from the current build outputs. +# Lint shell tooling and run the shared APT helper regression check. +lint-shell: + #!/usr/bin/env bash + set -euo pipefail + command -v pre-commit >/dev/null || { echo "error: pre-commit not found. Run 'just setup' first."; exit 1; } + pre-commit run shellcheck --all-files + pre-commit run apt-common-regression --all-files + +# Lint GitHub Actions workflows and run workflow regression checks. +lint-workflows: + #!/usr/bin/env bash + set -euo pipefail + command -v pre-commit >/dev/null || { echo "error: pre-commit not found. Run 'just setup' first."; exit 1; } + pre-commit run actionlint --all-files + pre-commit run workflow-regressions --all-files + +# Run the repository's shell and workflow lint suite. +lint-tooling: lint-shell lint-workflows + +# Build a local Linux package from baseline-compatible APT artifacts. [linux] -package-local format=DEFAULT_PACKAGE_FORMAT revision="1": (build-msb "release") build-libkrunfw +package-local format=DEFAULT_PACKAGE_FORMAT revision="1" image="": #!/usr/bin/env bash set -euo pipefail arch="$(uname -m)" package_format="{{ format }}" revision="{{ revision }}" + baseline_image="{{ image }}" version="$(sed -n 's/^version = "\(.*\)"$/\1/p' Cargo.toml | head -n1)" output_dir="{{ LOCAL_PACKAGE_DIST_DIR }}/$package_format" + artifacts_dir="build/apt/$arch" test -n "$version" || { echo "error: could not determine workspace version from Cargo.toml"; exit 1; } + baseline_cmd=(bash scripts/build-apt-baseline-artifacts.sh --output-dir "$artifacts_dir") + if [ -n "$baseline_image" ]; then + baseline_cmd+=(--image "$baseline_image") + fi + + echo "==> Building baseline-compatible APT artifacts for $arch..." + "${baseline_cmd[@]}" + case "$package_format" in deb) echo "==> Packaging {{ LINUX_PACKAGE_NAME }} $version-$revision as $package_format..." @@ -282,8 +311,8 @@ package-local format=DEFAULT_PACKAGE_FORMAT revision="1": (build-msb "release") --arch "$arch" \ --version "$version" \ --revision "$revision" \ - --msb "build/msb" \ - --libkrunfw "build/libkrunfw.so.{{ LIBKRUNFW_VERSION }}" \ + --msb "$artifacts_dir/msb" \ + --libkrunfw "$artifacts_dir/libkrunfw.so.{{ LIBKRUNFW_VERSION }}" \ --output-dir "$output_dir" ;; *) From 2400411cc3d8c517957b9f734693afed193ef881 Mon Sep 17 00:00:00 2001 From: Leavi15 Date: Sat, 18 Apr 2026 18:25:40 -0600 Subject: [PATCH 19/25] feat(scripts): add shared library for common APT utility functions --- scripts/lib/apt-common.sh | 54 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 scripts/lib/apt-common.sh diff --git a/scripts/lib/apt-common.sh b/scripts/lib/apt-common.sh new file mode 100644 index 00000000..af82b6c2 --- /dev/null +++ b/scripts/lib/apt-common.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash + +if [[ -n "${MICROSANDBOX_APT_COMMON_SH_LOADED:-}" ]]; then + return 0 +fi +MICROSANDBOX_APT_COMMON_SH_LOADED=1 + +require_cmd() { + command -v "$1" >/dev/null 2>&1 || { + echo "error: required command not found: $1" >&2 + exit 1 + } +} + +map_deb_arch() { + case "$1" in + amd64 | x86_64) printf '%s\n' "amd64" ;; + arm64 | aarch64) printf '%s\n' "arm64" ;; + *) + echo "error: unsupported Debian architecture: $1" >&2 + exit 1 + ;; + esac +} + +normalize_deb_version() { + local raw="$1" + local revision="${2:-1}" + local clean="${raw#v}" + + if [[ "$clean" == *-* ]]; then + printf '%s\n' "$clean" + return + fi + + printf '%s-%s\n' "$clean" "$revision" +} + +render_template() { + local template="$1" + shift + + local rendered + rendered="$(<"$template")" + + while [[ $# -gt 0 ]]; do + local key="$1" + local value="$2" + rendered="${rendered//${key}/${value}}" + shift 2 + done + + printf '%s' "$rendered" +} From e1938eb6ff2f2de335775aa5541fa069af29faf2 Mon Sep 17 00:00:00 2001 From: Leavi15 Date: Sat, 18 Apr 2026 18:25:57 -0600 Subject: [PATCH 20/25] feat(scripts): add shared library for common APT utility functions --- scripts/apt-smoke-test.sh | 11 ++--- scripts/build-apt-baseline-artifacts.sh | 13 +++--- scripts/build-apt-repo.sh | 30 +++----------- scripts/generate-apt-test-key.sh | 9 +++-- scripts/import-apt-signing-key.sh | 9 +++-- scripts/package-deb.sh | 54 +++---------------------- scripts/test-apt-repo.sh | 11 ++--- scripts/validate-deb.sh | 35 +++------------- 8 files changed, 38 insertions(+), 134 deletions(-) diff --git a/scripts/apt-smoke-test.sh b/scripts/apt-smoke-test.sh index a4695181..85942548 100755 --- a/scripts/apt-smoke-test.sh +++ b/scripts/apt-smoke-test.sh @@ -1,6 +1,10 @@ #!/usr/bin/env bash set -euo pipefail +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=lib/apt-common.sh +source "$SCRIPT_DIR/lib/apt-common.sh" + usage() { cat <<'EOF' Usage: scripts/apt-smoke-test.sh --repo-url [--keyring-path | --key-url ] @@ -10,13 +14,6 @@ test on a Linux host with KVM. EOF } -require_cmd() { - command -v "$1" >/dev/null 2>&1 || { - echo "error: required command not found: $1" >&2 - exit 1 - } -} - REPO_URL="" KEYRING_PATH="" KEY_URL="" diff --git a/scripts/build-apt-baseline-artifacts.sh b/scripts/build-apt-baseline-artifacts.sh index ff2dfead..dd341a28 100644 --- a/scripts/build-apt-baseline-artifacts.sh +++ b/scripts/build-apt-baseline-artifacts.sh @@ -1,6 +1,10 @@ #!/usr/bin/env bash set -euo pipefail +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=lib/apt-common.sh +source "$SCRIPT_DIR/lib/apt-common.sh" + usage() { cat <<'EOF' Usage: scripts/build-apt-baseline-artifacts.sh --output-dir [--image ] @@ -10,13 +14,6 @@ the resulting package remains compatible with the supported Debian/Ubuntu matrix EOF } -require_cmd() { - command -v "$1" >/dev/null 2>&1 || { - echo "error: required command not found: $1" >&2 - exit 1 - } -} - OUTPUT_DIR="" BASELINE_IMAGE="${APT_BASELINE_IMAGE:-docker.io/library/rust:1-bullseye}" CONTAINER_RUNTIME="${CONTAINER_RUNTIME:-docker}" @@ -51,7 +48,7 @@ done require_cmd "$CONTAINER_RUNTIME" -REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" OUTPUT_DIR_ABS="$(mkdir -p "$OUTPUT_DIR" && cd "$OUTPUT_DIR" && pwd)" HOST_UID="$(id -u)" HOST_GID="$(id -g)" diff --git a/scripts/build-apt-repo.sh b/scripts/build-apt-repo.sh index 61c86a43..ba67f517 100755 --- a/scripts/build-apt-repo.sh +++ b/scripts/build-apt-repo.sh @@ -1,6 +1,10 @@ #!/usr/bin/env bash set -euo pipefail +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=lib/apt-common.sh +source "$SCRIPT_DIR/lib/apt-common.sh" + usage() { cat <<'EOF' Usage: scripts/build-apt-repo.sh --input-dir --output-dir --gpg-key-id @@ -9,30 +13,6 @@ Build and sign a static APT repository from one or more `.deb` files. EOF } -require_cmd() { - command -v "$1" >/dev/null 2>&1 || { - echo "error: required command not found: $1" >&2 - exit 1 - } -} - -render_template() { - local template="$1" - shift - - local rendered - rendered="$(<"$template")" - - while [[ $# -gt 0 ]]; do - local key="$1" - local value="$2" - rendered="${rendered//${key}/${value}}" - shift 2 - done - - printf '%s' "$rendered" -} - INPUT_DIR="" OUTPUT_DIR="" GPG_KEY_ID="" @@ -109,7 +89,7 @@ require_cmd dpkg-scanpackages require_cmd gpg require_cmd gzip -REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" TEMPLATE_DIR="$REPO_ROOT/packaging/apt" SOURCE_DATE_EPOCH="${SOURCE_DATE_EPOCH:-$(git -C "$REPO_ROOT" log -1 --format=%ct 2>/dev/null || date +%s)}" diff --git a/scripts/generate-apt-test-key.sh b/scripts/generate-apt-test-key.sh index e83cdc8a..ece8bdaf 100755 --- a/scripts/generate-apt-test-key.sh +++ b/scripts/generate-apt-test-key.sh @@ -1,6 +1,10 @@ #!/usr/bin/env bash set -euo pipefail +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=lib/apt-common.sh +source "$SCRIPT_DIR/lib/apt-common.sh" + usage() { cat <<'EOF' Usage: scripts/generate-apt-test-key.sh --gnupg-home @@ -44,10 +48,7 @@ done exit 1 } -command -v gpg >/dev/null 2>&1 || { - echo "error: required command not found: gpg" >&2 - exit 1 -} +require_cmd gpg mkdir -p "$GNUPG_HOME" chmod 700 "$GNUPG_HOME" diff --git a/scripts/import-apt-signing-key.sh b/scripts/import-apt-signing-key.sh index daed1a6b..b2107a6b 100755 --- a/scripts/import-apt-signing-key.sh +++ b/scripts/import-apt-signing-key.sh @@ -1,6 +1,10 @@ #!/usr/bin/env bash set -euo pipefail +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=lib/apt-common.sh +source "$SCRIPT_DIR/lib/apt-common.sh" + usage() { cat <<'EOF' Usage: scripts/import-apt-signing-key.sh --gnupg-home [--private-key-file ] @@ -41,10 +45,7 @@ done exit 1 } -command -v gpg >/dev/null 2>&1 || { - echo "error: required command not found: gpg" >&2 - exit 1 -} +require_cmd gpg mkdir -p "$GNUPG_HOME" chmod 700 "$GNUPG_HOME" diff --git a/scripts/package-deb.sh b/scripts/package-deb.sh index 40bdb38f..0c09c03a 100755 --- a/scripts/package-deb.sh +++ b/scripts/package-deb.sh @@ -1,6 +1,10 @@ #!/usr/bin/env bash set -euo pipefail +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=lib/apt-common.sh +source "$SCRIPT_DIR/lib/apt-common.sh" + usage() { cat <<'EOF' Usage: scripts/package-deb.sh --arch --version --msb \ @@ -10,54 +14,6 @@ Build a Debian package for microsandbox from Linux release artifacts. EOF } -require_cmd() { - command -v "$1" >/dev/null 2>&1 || { - echo "error: required command not found: $1" >&2 - exit 1 - } -} - -map_deb_arch() { - case "$1" in - amd64 | x86_64) echo "amd64" ;; - arm64 | aarch64) echo "arm64" ;; - *) - echo "error: unsupported Debian architecture: $1" >&2 - exit 1 - ;; - esac -} - -normalize_version() { - local raw="$1" - local revision="$2" - local clean="${raw#v}" - - if [[ "$clean" == *-* ]]; then - printf '%s\n' "$clean" - return - fi - - printf '%s-%s\n' "$clean" "$revision" -} - -render_template() { - local template="$1" - shift - - local rendered - rendered="$(<"$template")" - - while [[ $# -gt 0 ]]; do - local key="$1" - local value="$2" - rendered="${rendered//${key}/${value}}" - shift 2 - done - - printf '%s' "$rendered" -} - PACKAGE_NAME="microsandbox" ARCH="" VERSION="" @@ -130,7 +86,7 @@ require_cmd sed REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" TEMPLATE_DIR="$REPO_ROOT/packaging/apt" DEB_ARCH="$(map_deb_arch "$ARCH")" -DEB_VERSION="$(normalize_version "$VERSION" "$REVISION")" +DEB_VERSION="$(normalize_deb_version "$VERSION" "$REVISION")" LIBKRUNFW_BASENAME="$(basename "$LIBKRUNFW_PATH")" if [[ "$LIBKRUNFW_BASENAME" =~ ^libkrunfw\.so\.([0-9]+)\..+$ ]]; then LIBKRUNFW_ABI="${BASH_REMATCH[1]}" diff --git a/scripts/test-apt-repo.sh b/scripts/test-apt-repo.sh index 9ca48d8c..42e64838 100755 --- a/scripts/test-apt-repo.sh +++ b/scripts/test-apt-repo.sh @@ -1,6 +1,10 @@ #!/usr/bin/env bash set -euo pipefail +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=lib/apt-common.sh +source "$SCRIPT_DIR/lib/apt-common.sh" + usage() { cat <<'EOF' Usage: scripts/test-apt-repo.sh --repo-v1 --repo-v2 --keyring \ @@ -11,13 +15,6 @@ against a local signed APT repository inside Debian/Ubuntu containers. EOF } -require_cmd() { - command -v "$1" >/dev/null 2>&1 || { - echo "error: required command not found: $1" >&2 - exit 1 - } -} - canonical_path() { realpath "$1" } diff --git a/scripts/validate-deb.sh b/scripts/validate-deb.sh index eaed0eea..e6d131d3 100755 --- a/scripts/validate-deb.sh +++ b/scripts/validate-deb.sh @@ -1,6 +1,10 @@ #!/usr/bin/env bash set -euo pipefail +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=lib/apt-common.sh +source "$SCRIPT_DIR/lib/apt-common.sh" + usage() { cat <<'EOF' Usage: scripts/validate-deb.sh --deb --arch --version @@ -9,35 +13,6 @@ Run structural validation and lint checks for a microsandbox Debian package. EOF } -require_cmd() { - command -v "$1" >/dev/null 2>&1 || { - echo "error: required command not found: $1" >&2 - exit 1 - } -} - -map_deb_arch() { - case "$1" in - amd64 | x86_64) echo "amd64" ;; - arm64 | aarch64) echo "arm64" ;; - *) - echo "error: unsupported Debian architecture: $1" >&2 - exit 1 - ;; - esac -} - -normalize_version() { - local raw="$1" - local revision="${2:-1}" - local clean="${raw#v}" - if [[ "$clean" == *-* ]]; then - printf '%s\n' "$clean" - else - printf '%s-%s\n' "$clean" "$revision" - fi -} - DEB_PATH="" ARCH="" VERSION="" @@ -89,7 +64,7 @@ require_cmd readlink } EXPECTED_ARCH="$(map_deb_arch "$ARCH")" -EXPECTED_VERSION="$(normalize_version "$VERSION" "$REVISION")" +EXPECTED_VERSION="$(normalize_deb_version "$VERSION" "$REVISION")" [[ "$(dpkg-deb -f "$DEB_PATH" Package)" == "microsandbox" ]] [[ "$(dpkg-deb -f "$DEB_PATH" Architecture)" == "$EXPECTED_ARCH" ]] From 1758c35592bfb58ba0498caa733b59fd8c0e627f Mon Sep 17 00:00:00 2001 From: Leavi15 Date: Sat, 18 Apr 2026 18:26:25 -0600 Subject: [PATCH 21/25] feat(ci): add configuration for actionlint on self-hosted runner --- actionlint.yaml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 actionlint.yaml diff --git a/actionlint.yaml b/actionlint.yaml new file mode 100644 index 00000000..25b5b921 --- /dev/null +++ b/actionlint.yaml @@ -0,0 +1,3 @@ +self-hosted-runner: + labels: + - self-hosted-ubuntu-2404-x64 From c31b23405dd89405d7f6c019eed87f042159b2a5 Mon Sep 17 00:00:00 2001 From: Leavi15 Date: Sat, 18 Apr 2026 18:26:27 -0600 Subject: [PATCH 22/25] feat(scripts): add tests for apt and ci workflows --- scripts/tests/test-apt-common.sh | 37 +++++++++++++++++ scripts/tests/test-workflows.py | 69 ++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+) create mode 100644 scripts/tests/test-apt-common.sh create mode 100644 scripts/tests/test-workflows.py diff --git a/scripts/tests/test-apt-common.sh b/scripts/tests/test-apt-common.sh new file mode 100644 index 00000000..74816a23 --- /dev/null +++ b/scripts/tests/test-apt-common.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=../lib/apt-common.sh +source "$SCRIPT_DIR/../lib/apt-common.sh" + +assert_eq() { + local actual="$1" + local expected="$2" + local message="$3" + + if [[ "$actual" != "$expected" ]]; then + echo "assertion failed: $message" >&2 + echo " expected: $expected" >&2 + echo " actual: $actual" >&2 + exit 1 + fi +} + +assert_eq "$(map_deb_arch x86_64)" "amd64" "x86_64 maps to amd64" +assert_eq "$(map_deb_arch aarch64)" "arm64" "aarch64 maps to arm64" +assert_eq "$(normalize_deb_version v1.2.3 4)" "1.2.3-4" "v-prefixed versions gain revision" +assert_eq "$(normalize_deb_version 1.2.3-2 4)" "1.2.3-2" "existing Debian revisions are preserved" + +tmpdir="$(mktemp -d)" +trap 'rm -rf "$tmpdir"' EXIT + +template="$tmpdir/control.template" +cat >"$template" <<'EOF' +Package: @PACKAGE@ +Version: @VERSION@ +EOF + +rendered="$(render_template "$template" "@PACKAGE@" "microsandbox" "@VERSION@" "1.2.3-1")" +expected=$'Package: microsandbox\nVersion: 1.2.3-1' +assert_eq "$rendered" "$expected" "template placeholders are rendered" diff --git a/scripts/tests/test-workflows.py b/scripts/tests/test-workflows.py new file mode 100644 index 00000000..a6167069 --- /dev/null +++ b/scripts/tests/test-workflows.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 + +from pathlib import Path +import sys + +import yaml + + +REPO_ROOT = Path(__file__).resolve().parents[2] +CHECK_WORKFLOW = REPO_ROOT / ".github/workflows/check.yml" +RELEASE_WORKFLOW = REPO_ROOT / ".github/workflows/release.yml" + + +def load_workflow(path: Path) -> dict: + with path.open("r", encoding="utf-8") as handle: + return yaml.safe_load(handle) + + +def workflow_on(document: dict) -> dict: + return document.get("on", document.get(True, {})) + + +def find_checkout_step(document: dict, job_name: str) -> dict | None: + steps = document["jobs"][job_name]["steps"] + return next((step for step in steps if step.get("uses") == "actions/checkout@v4"), None) + + +def main() -> int: + failures: list[str] = [] + + check_text = CHECK_WORKFLOW.read_text(encoding="utf-8") + release_text = RELEASE_WORKFLOW.read_text(encoding="utf-8") + check = load_workflow(CHECK_WORKFLOW) + release = load_workflow(RELEASE_WORKFLOW) + + if workflow_on(release).get("push", {}).get("tags") != ["v*"]: + failures.append("release.yml must publish from push tags ['v*']") + + if "workflow_run" in release_text: + failures.append("release.yml must not depend on workflow_run context") + + if "prepare" in release.get("jobs", {}): + failures.append("release.yml must not gate publication behind a prepare job") + + if "release-metadata" in check_text: + failures.append("check.yml must not carry unreachable release metadata artifacts") + + for document, path, job_name in [ + (check, CHECK_WORKFLOW, "apt-package-test"), + (release, RELEASE_WORKFLOW, "apt-package"), + ]: + checkout = find_checkout_step(document, job_name) + if checkout is None: + failures.append(f"{path.name}:{job_name} must define an actions/checkout@v4 step") + continue + if checkout.get("with", {}).get("submodules") is not True: + failures.append(f"{path.name}:{job_name} checkout must enable submodules") + + if failures: + for failure in failures: + print(f"FAIL: {failure}", file=sys.stderr) + return 1 + + print("workflow structure checks passed") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From 0b4ba34ae3f5f929cf2ddc91d1e282e685418d73 Mon Sep 17 00:00:00 2001 From: Leavi15 Date: Sat, 18 Apr 2026 18:28:08 -0600 Subject: [PATCH 23/25] feat(ci): add tooling lint job and integrate into workflow --- .github/workflows/check.yml | 52 +++++++++++++++++++++++++++---------- 1 file changed, 38 insertions(+), 14 deletions(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 8c068d70..05c97bcf 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -34,6 +34,41 @@ jobs: code: - '!docs/**' + # --------------------------------------------------------------------------- + # Lint workflow and packaging tooling + # --------------------------------------------------------------------------- + tooling-lint: + name: Tooling Lint + needs: changes + if: needs.changes.outputs.code == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: true + + - uses: actions/setup-go@v5 + with: + go-version: stable + + - name: Install workflow and shell lint dependencies + run: sudo apt-get update && sudo apt-get install -y python3-yaml shellcheck + + - name: Install actionlint + run: go install github.com/rhysd/actionlint/cmd/actionlint@v1.7.12 + + - name: Run shared packaging helper regression test + run: bash scripts/tests/test-apt-common.sh + + - name: Run workflow structure regression test + run: python3 scripts/tests/test-workflows.py + + - name: Lint shell scripts + run: PATH="$HOME/go/bin:$PATH" shellcheck -x scripts/*.sh scripts/lib/*.sh scripts/tests/*.sh + + - name: Lint GitHub Actions workflows + run: PATH="$HOME/go/bin:$PATH" actionlint + # --------------------------------------------------------------------------- # Build kernel.c on Linux for macOS libkrunfw linking # --------------------------------------------------------------------------- @@ -108,7 +143,7 @@ jobs: # --------------------------------------------------------------------------- check: name: Check (${{ matrix.target }}) - needs: [build-kernel, build-agentd-aarch64, changes] + needs: [tooling-lint, build-kernel, build-agentd-aarch64, changes] if: always() && needs.changes.outputs.code == 'true' runs-on: ${{ matrix.runner }} strategy: @@ -304,19 +339,6 @@ jobs: sdk/node-ts/tsconfig.json sdk/node-ts/vitest.config.ts - - name: Write release metadata - if: matrix.target == 'linux-x86_64' && startsWith(github.ref, 'refs/tags/v') - run: | - mkdir -p release-metadata - printf '%s\n' "${GITHUB_REF_NAME}" > release-metadata/release-tag.txt - - - name: Upload release metadata - if: matrix.target == 'linux-x86_64' && startsWith(github.ref, 'refs/tags/v') - uses: actions/upload-artifact@v4 - with: - name: release-metadata - path: release-metadata/release-tag.txt - # -- Checks (MCP server) -- - name: Build MCP server working-directory: mcp @@ -353,6 +375,8 @@ jobs: upload_kvm_repo: false steps: - uses: actions/checkout@v4 + with: + submodules: true - name: Install APT packaging dependencies run: | From ee9b98f2ee46d851c23e247a2bcc2d2840cc06f2 Mon Sep 17 00:00:00 2001 From: Leavi15 Date: Sat, 18 Apr 2026 18:30:14 -0600 Subject: [PATCH 24/25] feat(ci): simplify release workflow by removing unused `prepare` job --- .github/workflows/release.yml | 89 +++++------------------------------ 1 file changed, 13 insertions(+), 76 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0f2b24a2..638816d4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,61 +11,21 @@ env: APT_REPO_DOMAIN: "apt.microsandbox.dev" permissions: - actions: read contents: write packages: write pages: write id-token: write jobs: - # --------------------------------------------------------------------------- - # Prepare release context from the successful Check workflow run - # --------------------------------------------------------------------------- - prepare: - name: Prepare Release Context - if: github.event.workflow_run.conclusion == 'success' - runs-on: ubuntu-latest - outputs: - release_tag: ${{ steps.context.outputs.release_tag }} - source_sha: ${{ steps.context.outputs.source_sha }} - steps: - - name: Resolve release tag - id: context - env: - GH_TOKEN: ${{ github.token }} - run: | - SOURCE_SHA="${{ github.event.workflow_run.head_sha }}" - RELEASE_TAG="" - - mkdir -p release-metadata - if gh run download "${{ github.event.workflow_run.id }}" \ - --repo "${{ github.repository }}" \ - --name release-metadata \ - --dir release-metadata; then - RELEASE_TAG="$(tr -d '\n' < release-metadata/release-tag.txt)" - fi - - echo "source_sha=$SOURCE_SHA" >> "$GITHUB_OUTPUT" - echo "release_tag=$RELEASE_TAG" >> "$GITHUB_OUTPUT" - - if [ -n "$RELEASE_TAG" ]; then - echo "Preparing release for $RELEASE_TAG from Check run ${{ github.event.workflow_run.id }}" - else - echo "Check succeeded without release metadata; skipping release workflow." - fi - # --------------------------------------------------------------------------- # Build kernel.c on Linux for macOS libkrunfw linking # --------------------------------------------------------------------------- build-kernel: name: Build kernel.c (aarch64) - needs: prepare - if: needs.prepare.outputs.release_tag != '' runs-on: ubuntu-24.04-arm steps: - uses: actions/checkout@v4 with: - ref: ${{ needs.prepare.outputs.source_sha }} submodules: true - name: Cache kernel.c @@ -96,13 +56,10 @@ jobs: # --------------------------------------------------------------------------- build-agentd-aarch64: name: Build agentd (aarch64-linux-musl) - needs: prepare - if: needs.prepare.outputs.release_tag != '' runs-on: ubuntu-24.04-arm steps: - uses: actions/checkout@v4 with: - ref: ${{ needs.prepare.outputs.source_sha }} submodules: true - uses: dtolnay/rust-toolchain@stable @@ -130,8 +87,8 @@ jobs: # --------------------------------------------------------------------------- build: name: Build (${{ matrix.target }}) - needs: [prepare, build-kernel, build-agentd-aarch64] - if: needs.prepare.outputs.release_tag != '' + needs: [build-kernel, build-agentd-aarch64] + if: always() runs-on: ${{ matrix.runner }} strategy: fail-fast: false @@ -171,7 +128,6 @@ jobs: steps: - uses: actions/checkout@v4 with: - ref: ${{ needs.prepare.outputs.source_sha }} submodules: true - uses: dtolnay/rust-toolchain@stable @@ -340,8 +296,6 @@ jobs: # --------------------------------------------------------------------------- apt-package: name: Package APT (${{ matrix.target }}) - needs: prepare - if: needs.prepare.outputs.release_tag != '' runs-on: ${{ matrix.runner }} strategy: fail-fast: false @@ -356,7 +310,7 @@ jobs: steps: - uses: actions/checkout@v4 with: - ref: ${{ needs.prepare.outputs.source_sha }} + submodules: true - name: Install APT packaging dependencies run: | @@ -372,7 +326,7 @@ jobs: run: | bash scripts/package-deb.sh \ --arch "${{ matrix.arch }}" \ - --version "${{ needs.prepare.outputs.release_tag }}" \ + --version "${{ github.ref_name }}" \ --revision 1 \ --msb build/apt/${{ matrix.arch }}/msb \ --libkrunfw build/apt/${{ matrix.arch }}/libkrunfw.so.${{ env.LIBKRUNFW_VERSION }} \ @@ -389,13 +343,10 @@ jobs: # --------------------------------------------------------------------------- apt-repo: name: Build APT Repository - needs: [prepare, apt-package] - if: needs.prepare.outputs.release_tag != '' + needs: [apt-package] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - with: - ref: ${{ needs.prepare.outputs.source_sha }} - name: Install APT repository tooling run: | @@ -449,8 +400,7 @@ jobs: # --------------------------------------------------------------------------- deploy-apt-repo: name: Deploy APT Repository - needs: [prepare, apt-repo] - if: needs.prepare.outputs.release_tag != '' + needs: [apt-repo] runs-on: ubuntu-latest environment: name: github-pages @@ -464,13 +414,10 @@ jobs: # --------------------------------------------------------------------------- assemble: name: Assemble Release - needs: [prepare, build, apt-package, deploy-apt-repo] - if: needs.prepare.outputs.release_tag != '' + needs: [build, apt-package, deploy-apt-repo] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - with: - ref: ${{ needs.prepare.outputs.source_sha }} - name: Download all artifacts uses: actions/download-artifact@v4 @@ -491,8 +438,8 @@ jobs: env: GH_TOKEN: ${{ github.token }} run: | - gh release create "${{ needs.prepare.outputs.release_tag }}" \ - --title "${{ needs.prepare.outputs.release_tag }}" \ + gh release create "${{ github.ref_name }}" \ + --title "${{ github.ref_name }}" \ --generate-notes \ release-artifacts/* @@ -516,13 +463,10 @@ jobs: # --------------------------------------------------------------------------- npm-publish: name: Publish npm packages - needs: [prepare, build] - if: needs.prepare.outputs.release_tag != '' + needs: [build] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - with: - ref: ${{ needs.prepare.outputs.source_sha }} - uses: actions/setup-node@v4 with: @@ -625,13 +569,11 @@ jobs: # --------------------------------------------------------------------------- mcp-publish: name: Publish MCP server - needs: [prepare, npm-publish] - if: needs.prepare.outputs.release_tag != '' + needs: [npm-publish] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: - ref: ${{ needs.prepare.outputs.source_sha }} submodules: true - uses: actions/setup-node@v4 @@ -654,13 +596,11 @@ jobs: # --------------------------------------------------------------------------- crates-publish: name: Publish crates - needs: [prepare, build] - if: needs.prepare.outputs.release_tag != '' + needs: [build] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: - ref: ${{ needs.prepare.outputs.source_sha }} submodules: true - uses: dtolnay/rust-toolchain@stable @@ -690,8 +630,7 @@ jobs: # --------------------------------------------------------------------------- pypi-publish: name: Publish Python SDK - needs: [prepare, build] - if: needs.prepare.outputs.release_tag != '' + needs: [build] runs-on: ubuntu-latest permissions: actions: read @@ -702,8 +641,6 @@ jobs: url: https://pypi.org/p/microsandbox steps: - uses: actions/checkout@v4 - with: - ref: ${{ needs.prepare.outputs.source_sha }} - uses: astral-sh/setup-uv@v3 From b28d14e82a3b7893a045f6288ec06c66e7409c36 Mon Sep 17 00:00:00 2001 From: Leavi15 Date: Tue, 5 May 2026 15:06:28 -0600 Subject: [PATCH 25/25] feat(ci): add `APT Canary` workflow for automated smoke testing --- .github/workflows/apt-canary.yml | 35 ++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 .github/workflows/apt-canary.yml diff --git a/.github/workflows/apt-canary.yml b/.github/workflows/apt-canary.yml new file mode 100644 index 00000000..f654abc7 --- /dev/null +++ b/.github/workflows/apt-canary.yml @@ -0,0 +1,35 @@ +name: APT Canary + +on: + schedule: + - cron: "0 9 * * *" + workflow_dispatch: + +jobs: + canary: + name: APT Canary Smoke Test + if: vars.APT_CANARY_ENABLED == 'true' || github.event_name == 'workflow_dispatch' + runs-on: self-hosted-ubuntu-2404-x64 + env: + APT_REPO_URL: ${{ vars.APT_REPO_URL }} + APT_REPO_KEY_URL: ${{ vars.APT_REPO_KEY_URL }} + DEFAULT_APT_REPO_URL: https://apt.microsandbox.dev + DEFAULT_APT_REPO_KEY_URL: https://apt.microsandbox.dev/microsandbox-archive-keyring.gpg + steps: + - uses: actions/checkout@v4 + + - name: Clean previous apt state + run: | + sudo apt-get remove -y microsandbox || true + sudo rm -f /etc/apt/sources.list.d/microsandbox.list + sudo rm -f /usr/share/keyrings/microsandbox-archive-keyring.gpg + rm -rf ~/.microsandbox + + - name: Install and smoke test from published APT repository + run: | + REPO_URL="${APT_REPO_URL:-$DEFAULT_APT_REPO_URL}" + KEY_URL="${APT_REPO_KEY_URL:-$DEFAULT_APT_REPO_KEY_URL}" + + bash scripts/apt-smoke-test.sh \ + --repo-url "$REPO_URL" \ + --key-url "$KEY_URL"