← back to Cli Printing Press
Reject stale publish manifests before packaging (#836)
5155331c0d177c74e7a18e174df5b3a3a59df4ec · 2026-05-09 12:41:07 -0700 · Trevin Chow
Files touched
M internal/cli/publish.goM internal/cli/publish_test.goM internal/pipeline/climanifest.goM internal/pipeline/publish.goM skills/printing-press-publish/SKILL.md
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 →