[object Object]

← back to Cli Printing Press

Reject stale publish manifests before packaging (#836)

5155331c0d177c74e7a18e174df5b3a3a59df4ec · 2026-05-09 12:41:07 -0700 · Trevin Chow

Files touched

Diff

commit 5155331c0d177c74e7a18e174df5b3a3a59df4ec
Author: Trevin Chow <trevin@trevinchow.com>
Date:   Sat May 9 12:41:07 2026 -0700

    Reject stale publish manifests before packaging (#836)
---
 internal/cli/publish.go                |  58 ++++++++++++++--
 internal/cli/publish_test.go           | 117 +++++++++++++++++++++++++++++----
 internal/pipeline/climanifest.go       |   5 +-
 internal/pipeline/publish.go           |   2 +-
 skills/printing-press-publish/SKILL.md |  12 +++-
 5 files changed, 173 insertions(+), 21 deletions(-)

diff --git a/internal/cli/publish.go b/internal/cli/publish.go
index 149a88e7..e65fd9ce 100644
--- a/internal/cli/publish.go
+++ b/internal/cli/publish.go
@@ -499,13 +499,13 @@ func runValidation(dir string) ValidateResult {
 			result.Checks = append(result.Checks, CheckResult{Name: "manifest", Passed: false, Error: fmt.Sprintf("invalid JSON: %v", err)})
 			allPassed = false
 		} else {
-			if manifest.APIName == "" || manifest.CLIName == "" {
-				result.Checks = append(result.Checks, CheckResult{Name: "manifest", Passed: false, Error: "missing required fields (api_name, cli_name)"})
+			result.CLIName = manifest.CLIName
+			result.APIName = manifest.APIName
+			if issues := validatePublishManifestContract(dir, manifest); len(issues) > 0 {
+				result.Checks = append(result.Checks, CheckResult{Name: "manifest", Passed: false, Error: strings.Join(issues, "; ")})
 				allPassed = false
 			} else {
 				result.Checks = append(result.Checks, CheckResult{Name: "manifest", Passed: true})
-				result.CLIName = manifest.CLIName
-				result.APIName = manifest.APIName
 			}
 		}
 	}
@@ -650,6 +650,56 @@ func runValidation(dir string) ValidateResult {
 	return result
 }
 
+func validatePublishManifestContract(dir string, manifest pipeline.CLIManifest) []string {
+	var issues []string
+	if manifest.SchemaVersion != pipeline.CurrentCLIManifestSchemaVersion {
+		issues = append(issues, fmt.Sprintf("schema_version must be %d (found %d)", pipeline.CurrentCLIManifestSchemaVersion, manifest.SchemaVersion))
+	}
+
+	var missing []string
+	required := []struct {
+		name  string
+		value string
+	}{
+		{name: "api_name", value: manifest.APIName},
+		{name: "cli_name", value: manifest.CLIName},
+		{name: "run_id", value: manifest.RunID},
+		{name: "printing_press_version", value: manifest.PrintingPressVersion},
+		{name: "printer", value: manifest.Printer},
+		{name: "printer_name", value: manifest.PrinterName},
+	}
+	for _, field := range required {
+		if strings.TrimSpace(field.value) == "" {
+			missing = append(missing, field.name)
+		}
+	}
+	if len(missing) > 0 {
+		issues = append(issues, "missing required manifest fields: "+strings.Join(missing, ", "))
+	}
+	if isPublishPrinterSentinel(manifest.Printer) {
+		issues = append(issues, fmt.Sprintf("printer must not be the literal sentinel %q", manifest.Printer))
+	}
+
+	if manifestAdvertisesMCP(manifest) {
+		for _, filename := range []string{pipeline.MCPBManifestFilename, pipeline.ToolsManifestFilename} {
+			path := filepath.Join(dir, filename)
+			if info, err := os.Stat(path); err != nil || info.IsDir() {
+				issues = append(issues, fmt.Sprintf("MCP package metadata missing %s", filename))
+			}
+		}
+	}
+
+	return issues
+}
+
+func isPublishPrinterSentinel(printer string) bool {
+	return printer == "USER" || printer == "user"
+}
+
+func manifestAdvertisesMCP(manifest pipeline.CLIManifest) bool {
+	return strings.TrimSpace(manifest.MCPBinary) != "" || manifest.MCPReady != "" || manifest.MCPToolCount > 0 || manifest.MCPPublicToolCount > 0
+}
+
 func checkPhase5Gate(dir string, manifest pipeline.CLIManifest) CheckResult {
 	if manifest.APIName == "" || manifest.CLIName == "" {
 		return CheckResult{Name: "phase5", Passed: false, Error: "manifest unavailable"}
diff --git a/internal/cli/publish_test.go b/internal/cli/publish_test.go
index 4314b5b7..c166de1c 100644
--- a/internal/cli/publish_test.go
+++ b/internal/cli/publish_test.go
@@ -5,6 +5,7 @@ import (
 	"os"
 	"path/filepath"
 	"runtime"
+	"strings"
 	"testing"
 
 	"github.com/mvanhorn/cli-printing-press/v4/internal/generator"
@@ -26,6 +27,17 @@ func skipIfRootCannotSimulateUnreadable(t *testing.T) {
 	}
 }
 
+func publishCheckByName(t *testing.T, result ValidateResult, name string) CheckResult {
+	t.Helper()
+	for _, check := range result.Checks {
+		if check.Name == name {
+			return check
+		}
+	}
+	t.Fatalf("missing %q check in %#v", name, result.Checks)
+	return CheckResult{}
+}
+
 func TestPublishValidateMissingManifest(t *testing.T) {
 	home := setLibraryTestEnv(t)
 	cliDir := filepath.Join(home, "library", "test-pp-cli")
@@ -82,7 +94,68 @@ func TestPublishValidateManifestMissingFields(t *testing.T) {
 	}
 	require.NotNil(t, manifestCheck)
 	assert.False(t, manifestCheck.Passed)
-	assert.Contains(t, manifestCheck.Error, "required fields")
+	assert.Contains(t, manifestCheck.Error, "missing required manifest fields")
+}
+
+func TestPublishValidateRejectsStaleAttributionManifest(t *testing.T) {
+	home := setLibraryTestEnv(t)
+	cliDir := filepath.Join(home, "library", "openrouter-pp-cli")
+	require.NoError(t, os.MkdirAll(cliDir, 0o755))
+
+	writeTestManifest(t, cliDir, pipeline.CLIManifest{
+		SchemaVersion:        0,
+		PrintingPressVersion: "4.2.0",
+		APIName:              "openrouter",
+		CLIName:              "openrouter-pp-cli",
+		RunID:                "20260509-165428",
+		Printer:              "rvdlaar",
+	})
+
+	cmd := newPublishCmd()
+	cmd.SetArgs([]string{"validate", "--dir", cliDir, "--json"})
+
+	output, err := runWithCapturedStdout(t, cmd.Execute)
+	require.Error(t, err)
+
+	var result ValidateResult
+	require.NoError(t, json.Unmarshal([]byte(output), &result))
+	assert.False(t, result.Passed)
+
+	manifestCheck := publishCheckByName(t, result, "manifest")
+	assert.False(t, manifestCheck.Passed)
+	assert.Contains(t, manifestCheck.Error, "schema_version must be 1")
+	assert.Contains(t, manifestCheck.Error, "printer_name")
+}
+
+func TestPublishManifestContractRejectsPrinterSentinel(t *testing.T) {
+	issues := validatePublishManifestContract(t.TempDir(), pipeline.CLIManifest{
+		SchemaVersion:        pipeline.CurrentCLIManifestSchemaVersion,
+		PrintingPressVersion: "4.2.1",
+		APIName:              "test",
+		CLIName:              "test-pp-cli",
+		RunID:                "20260509-000000",
+		Printer:              "USER",
+		PrinterName:          "Test User",
+	})
+
+	require.Len(t, issues, 1)
+	assert.Contains(t, issues[0], "literal sentinel")
+}
+
+func TestPublishManifestContractRequiresMCPMetadataFiles(t *testing.T) {
+	issues := validatePublishManifestContract(t.TempDir(), pipeline.CLIManifest{
+		SchemaVersion:        pipeline.CurrentCLIManifestSchemaVersion,
+		PrintingPressVersion: "4.2.1",
+		APIName:              "test",
+		CLIName:              "test-pp-cli",
+		RunID:                "20260509-000000",
+		Printer:              "tmchow",
+		PrinterName:          "Trevin Chow",
+		MCPBinary:            "test-pp-mcp",
+	})
+
+	assert.Contains(t, strings.Join(issues, "\n"), "manifest.json")
+	assert.Contains(t, strings.Join(issues, "\n"), "tools-manifest.json")
 }
 
 func TestPublishValidateMissingDirFlag(t *testing.T) {
@@ -165,11 +238,17 @@ func TestPublishValidateFailsWithoutPhase5Marker(t *testing.T) {
 	cliDir := filepath.Join(home, "library", "test-pp-cli")
 	writePublishableTestCLI(t, cliDir)
 	writeTestManifest(t, cliDir, pipeline.CLIManifest{
-		SchemaVersion: 1,
-		APIName:       "test",
-		CLIName:       "test-pp-cli",
-		RunID:         "run-missing-phase5",
-		AuthType:      "api_key",
+		SchemaVersion:        pipeline.CurrentCLIManifestSchemaVersion,
+		PrintingPressVersion: "test-version",
+		APIName:              "test",
+		CLIName:              "test-pp-cli",
+		RunID:                "run-missing-phase5",
+		Printer:              "tmchow",
+		PrinterName:          "Trevin Chow",
+		AuthType:             "api_key",
+		NovelFeatures: []pipeline.NovelFeatureManifest{
+			{Name: "Insight", Command: "insight", Description: "Show test insight."},
+		},
 	})
 
 	cmd := newPublishCmd()
@@ -200,9 +279,13 @@ func TestPublishValidateRequiresTranscendenceFeatures(t *testing.T) {
 	require.NoError(t, os.MkdirAll(cliDir, 0o755))
 
 	data, err := json.MarshalIndent(pipeline.CLIManifest{
-		SchemaVersion: 1,
-		APIName:       "test",
-		CLIName:       "test-pp-cli",
+		SchemaVersion:        pipeline.CurrentCLIManifestSchemaVersion,
+		PrintingPressVersion: "test-version",
+		APIName:              "test",
+		CLIName:              "test-pp-cli",
+		RunID:                "20260301-000000",
+		Printer:              "tmchow",
+		PrinterName:          "Trevin Chow",
 	}, "", "  ")
 	require.NoError(t, err)
 	require.NoError(t, os.WriteFile(filepath.Join(cliDir, pipeline.CLIManifestFilename), data, 0o644))
@@ -899,11 +982,17 @@ func newInsightCmd() *cobra.Command {
 	require.NoError(t, os.WriteFile(filepath.Join(dir, "SKILL.md"), []byte("# Test CLI\n\n"+skillInstall+"\n## Command Reference\n\n- `test-pp-cli insight` — Show test insight\n\n## Usage\n\n```bash\ntest-pp-cli insight --agent\n```\n"), 0o644))
 
 	writeTestManifest(t, dir, pipeline.CLIManifest{
-		SchemaVersion: 1,
-		APIName:       "test",
-		CLIName:       "test-pp-cli",
-		RunID:         "20260301-000000",
-		AuthType:      "none",
+		SchemaVersion:        pipeline.CurrentCLIManifestSchemaVersion,
+		PrintingPressVersion: "test-version",
+		APIName:              "test",
+		CLIName:              "test-pp-cli",
+		RunID:                "20260301-000000",
+		Printer:              "tmchow",
+		PrinterName:          "Trevin Chow",
+		AuthType:             "none",
+		NovelFeatures: []pipeline.NovelFeatureManifest{
+			{Name: "Insight", Command: "insight", Description: "Show test insight."},
+		},
 	})
 	writePublishablePhase5Pass(t)
 }
diff --git a/internal/pipeline/climanifest.go b/internal/pipeline/climanifest.go
index c0364bf2..637fa33c 100644
--- a/internal/pipeline/climanifest.go
+++ b/internal/pipeline/climanifest.go
@@ -26,6 +26,9 @@ import (
 // published CLI directory.
 const CLIManifestFilename = ".printing-press.json"
 
+// CurrentCLIManifestSchemaVersion is the public-library provenance contract.
+const CurrentCLIManifestSchemaVersion = 1
+
 // CLIManifest captures provenance metadata for a generated CLI.
 // It is written to the root of each published CLI directory so the
 // folder is self-describing even in isolation.
@@ -416,7 +419,7 @@ func DeriveRunIDFromResearchDir(researchDir string) string {
 // writeCLIManifestForPublish (which operates on PipelineState).
 func WriteManifestForGenerate(p GenerateManifestParams) error {
 	m := CLIManifest{
-		SchemaVersion:        1,
+		SchemaVersion:        CurrentCLIManifestSchemaVersion,
 		GeneratedAt:          time.Now().UTC(),
 		PrintingPressVersion: version.Version,
 		APIName:              p.APIName,
diff --git a/internal/pipeline/publish.go b/internal/pipeline/publish.go
index 346339b2..dc631f50 100644
--- a/internal/pipeline/publish.go
+++ b/internal/pipeline/publish.go
@@ -193,7 +193,7 @@ func writeCLIManifestForPublish(state *PipelineState, dir string) error {
 	}
 
 	m := CLIManifest{
-		SchemaVersion:        1,
+		SchemaVersion:        CurrentCLIManifestSchemaVersion,
 		GeneratedAt:          time.Now().UTC(),
 		PrintingPressVersion: version.Version,
 		APIName:              state.APIName,
diff --git a/skills/printing-press-publish/SKILL.md b/skills/printing-press-publish/SKILL.md
index c71f3c3d..63e9985b 100644
--- a/skills/printing-press-publish/SKILL.md
+++ b/skills/printing-press-publish/SKILL.md
@@ -216,6 +216,11 @@ Validating <api-slug>...
 ```
 
 If `"passed": false`, report the failing checks and **stop**. Do not create a partial PR.
+The `manifest` check is authoritative for the public-library provenance
+contract: current `schema_version`, `run_id`, `printing_press_version`,
+`printer`, `printer_name`, and MCP metadata files when MCP is advertised. If it
+fails, tell the user to re-print or re-package with current Printing Press
+metadata before opening the library PR.
 
 Save the `help_output` field from the result — it's used in the PR description.
 
@@ -415,8 +420,9 @@ cp -r "$STAGING_DIR/library/<category>/<cli-name>" "$PUBLISH_REPO_DIR/library/<c
 # Remove binaries (should not be committed)
 rm -f "$PUBLISH_REPO_DIR/library/<category>/<api-slug>/<api-slug>" "$PUBLISH_REPO_DIR/library/<category>/<api-slug>/<cli-name>"
 
-# Strict-validate printer attribution before it reaches README and registry surfaces.
+# Defense-in-depth: validate printer attribution before README and registry surfaces.
 PRINTER=$(jq -r '.printer // ""' "$PUBLISH_REPO_DIR/library/<category>/<api-slug>/.printing-press.json")
+PRINTER_NAME=$(jq -r '.printer_name // ""' "$PUBLISH_REPO_DIR/library/<category>/<api-slug>/.printing-press.json")
 if [ -z "$PRINTER" ]; then
   echo "ERROR: manifest .printer is empty. Set 'git config --global github.user <your-handle>' and re-print before publishing."
   exit 1
@@ -425,6 +431,10 @@ if [ "$PRINTER" = "USER" ] || [ "$PRINTER" = "user" ]; then
   echo "ERROR: manifest .printer is the literal sentinel \"$PRINTER\" (git config github.user was unset at print time). Set it and re-print before publishing."
   exit 1
 fi
+if [ -z "$PRINTER_NAME" ]; then
+  echo "ERROR: manifest .printer_name is empty. Set 'git config --global user.name <your display name>' and re-print before publishing."
+  exit 1
+fi
 
 # Regenerate the flat cli-skills mirror from the library tree so library PR CI passes mirror parity.
 if [ -f "$PUBLISH_REPO_DIR/tools/generate-skills/main.go" ]; then

← 1c0d110b fix(cli): preserve operation server host overrides (#834)  ·  back to Cli Printing Press  ·  Ignore .omx workspace folders (#841) 117544d0 →