← back to Cli Printing Press
fix(cli): strip build/ from publish staging (#502)
263be3b3d24ecae4a9995fda9b98de14dd7f2dfe · 2026-05-02 09:27:20 -0700 · Trevin Chow
`autoBundleForHost` writes a host-platform .mcpb plus the staged binary
into `<cliDir>/build/` after generate. That output is intended as
local-dev scratch — useful for testing the bundle in Claude Desktop on
the generating machine — but `printing-press publish package`'s
`pipeline.CopyDir` was unconditional, so build/ also reached the public
library through the publish path. That created two problems:
- The public library accumulates ~10 MB binary blobs per CLI per
generation, with no good way to track which platforms were built when.
- Different CLIs end up with different platform .mcpb files depending on
what host the publisher used; users on other platforms have no
bundles at all.
Going forward, the canonical multi-platform .mcpb artifacts are produced
by the public library's CI on merge to main and attached as release
assets, keyed by a moving `<slug>-current` tag the README links to.
build/ is local-dev only.
Adds a post-CopyDir RemoveAll on outCLIDir/build, a test that verifies
staging excludes build/ even when the source dir contains one, and a
docstring update on autoBundleForHost explaining the local-vs-CI split.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Files touched
M internal/cli/bundle.goM internal/cli/publish.goM internal/cli/publish_test.go
Diff
commit 263be3b3d24ecae4a9995fda9b98de14dd7f2dfe
Author: Trevin Chow <trevin@trevinchow.com>
Date: Sat May 2 09:27:20 2026 -0700
fix(cli): strip build/ from publish staging (#502)
`autoBundleForHost` writes a host-platform .mcpb plus the staged binary
into `<cliDir>/build/` after generate. That output is intended as
local-dev scratch — useful for testing the bundle in Claude Desktop on
the generating machine — but `printing-press publish package`'s
`pipeline.CopyDir` was unconditional, so build/ also reached the public
library through the publish path. That created two problems:
- The public library accumulates ~10 MB binary blobs per CLI per
generation, with no good way to track which platforms were built when.
- Different CLIs end up with different platform .mcpb files depending on
what host the publisher used; users on other platforms have no
bundles at all.
Going forward, the canonical multi-platform .mcpb artifacts are produced
by the public library's CI on merge to main and attached as release
assets, keyed by a moving `<slug>-current` tag the README links to.
build/ is local-dev only.
Adds a post-CopyDir RemoveAll on outCLIDir/build, a test that verifies
staging excludes build/ even when the source dir contains one, and a
docstring update on autoBundleForHost explaining the local-vs-CI split.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---
internal/cli/bundle.go | 5 +++++
internal/cli/publish.go | 10 ++++++++++
internal/cli/publish_test.go | 31 +++++++++++++++++++++++++++++++
3 files changed, 46 insertions(+)
diff --git a/internal/cli/bundle.go b/internal/cli/bundle.go
index 859caf2b..c0388675 100644
--- a/internal/cli/bundle.go
+++ b/internal/cli/bundle.go
@@ -119,6 +119,11 @@ from another build pipeline.`,
}
// autoBundleForHost packages a host-platform .mcpb after generate.
+// The output lives at <cliDir>/build/ as local-dev scratch — useful for
+// trying the bundle in Claude Desktop on the generating machine before
+// publishing. The canonical multi-platform .mcpb artifacts are produced by
+// the public library's CI on merge to main and attached as release assets;
+// `printing-press publish package` strips build/ from the staged tree.
// Best-effort: skips silently for expected non-bundle states (no manifest,
// no go.sum) and warns on real failures (malformed manifest, build/zip
// errors). Users can always re-run via `printing-press bundle <dir>`.
diff --git a/internal/cli/publish.go b/internal/cli/publish.go
index ee132d23..4e1bdad1 100644
--- a/internal/cli/publish.go
+++ b/internal/cli/publish.go
@@ -337,6 +337,16 @@ func newPublishPackageCmd() *cobra.Command {
return &ExitError{Code: ExitPublishError, Err: fmt.Errorf("copying CLI: %w", err)}
}
+ // Strip build/ from the staged tree. autoBundleForHost writes
+ // host-platform .mcpb bundles + staged binaries there as a
+ // local-dev convenience; the public library treats CI release
+ // artifacts as canonical, so build/ should never reach the
+ // public library through the publish path.
+ if err := os.RemoveAll(filepath.Join(outCLIDir, "build")); err != nil {
+ cleanupOnFailure()
+ return &ExitError{Code: ExitPublishError, Err: fmt.Errorf("stripping build dir: %w", err)}
+ }
+
// Rewrite go.mod module path if --module-path is set
if modulePath != "" {
oldModPath := cliName // generated CLIs use bare CLI name as module path
diff --git a/internal/cli/publish_test.go b/internal/cli/publish_test.go
index 72d018f2..ecf07096 100644
--- a/internal/cli/publish_test.go
+++ b/internal/cli/publish_test.go
@@ -377,6 +377,37 @@ func TestPublishPackageDoesNotStageCompiledBinary(t *testing.T) {
assert.ErrorIs(t, stagedErr, os.ErrNotExist, "packaged source should not include a compiled binary")
}
+func TestPublishPackageStripsBuildDir(t *testing.T) {
+ home := setLibraryTestEnv(t)
+ cliDir := filepath.Join(home, "library", "test-pp-cli")
+ writePublishableTestCLI(t, cliDir)
+
+ // Simulate autoBundleForHost output: build/ exists in the source
+ // dir with a host-platform .mcpb and a staged binary copy.
+ buildDir := filepath.Join(cliDir, "build")
+ require.NoError(t, os.MkdirAll(filepath.Join(buildDir, "stage", "bin"), 0o755))
+ require.NoError(t, os.WriteFile(filepath.Join(buildDir, "test-pp-mcp-darwin-arm64.mcpb"), []byte("zip-bytes"), 0o644))
+ require.NoError(t, os.WriteFile(filepath.Join(buildDir, "stage", "bin", "test-pp-mcp"), []byte("staged-binary"), 0o755))
+
+ target := filepath.Join(t.TempDir(), "staging")
+ cmd := newPublishCmd()
+ cmd.SetArgs([]string{"package", "--dir", cliDir, "--category", "other", "--target", target, "--json"})
+
+ output, err := runWithCapturedStdout(t, cmd.Execute)
+ require.NoError(t, err)
+
+ var result PackageResult
+ require.NoError(t, json.Unmarshal([]byte(output), &result))
+
+ // Source build/ stays intact — we don't touch the user's working tree.
+ _, sourceErr := os.Stat(buildDir)
+ assert.NoError(t, sourceErr, "package must not delete the source build/ dir")
+
+ // Staged tree must have no build/ — CI is canonical for distribution.
+ _, stagedErr := os.Stat(filepath.Join(result.StagedDir, "build"))
+ assert.ErrorIs(t, stagedErr, os.ErrNotExist, "staged dir must not include build/")
+}
+
func TestPublishPackageFailsWhenManuscriptsCopyFails(t *testing.T) {
skipIfRootCannotSimulateUnreadable(t)
home := setLibraryTestEnv(t)
← 2817bdc7 fix(cli): route resource base urls through mcp surfaces (#50
·
back to Cli Printing Press
·
fix(cli): route discriminator sync to typed tables (#503) 97180c03 →