← back to Cli Printing Press
fix(cli): publish manifest must read spec.yaml alongside spec.json (#220)
c5ed4369a52f331e5a3fcccf922c754f2f4b5027 · 2026-04-18 02:21:11 -0700 · Trevin Chow
writeCLIManifestForPublish only looked for spec.json in the working
directory, so YAML-archived CLIs (espn, hackernews, recipe-goat,
yahoo-finance, weather-goat, archive-is, movie-goat) silently lost
spec_format, spec_checksum, MCP metadata refresh, and tools-manifest.json
regeneration at publish time. The generate-time manifest path already
handled the multi-extension case; the publish-time path drifted.
Extracted findArchivedSpec(dir) for the spec.json -> spec.yaml -> spec.yml
lookup and used it in both call sites. Backfill of existing manifests is
a separate concern (requires a republish per CLI).
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Files touched
M internal/pipeline/climanifest.goM internal/pipeline/climanifest_test.goM internal/pipeline/publish.go
Diff
commit c5ed4369a52f331e5a3fcccf922c754f2f4b5027
Author: Trevin Chow <trevin@trevinchow.com>
Date: Sat Apr 18 02:21:11 2026 -0700
fix(cli): publish manifest must read spec.yaml alongside spec.json (#220)
writeCLIManifestForPublish only looked for spec.json in the working
directory, so YAML-archived CLIs (espn, hackernews, recipe-goat,
yahoo-finance, weather-goat, archive-is, movie-goat) silently lost
spec_format, spec_checksum, MCP metadata refresh, and tools-manifest.json
regeneration at publish time. The generate-time manifest path already
handled the multi-extension case; the publish-time path drifted.
Extracted findArchivedSpec(dir) for the spec.json -> spec.yaml -> spec.yml
lookup and used it in both call sites. Backfill of existing manifests is
a separate concern (requires a republish per CLI).
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---
internal/pipeline/climanifest.go | 30 +++++++++++++++++++--------
internal/pipeline/climanifest_test.go | 39 +++++++++++++++++++++++++++++++++++
internal/pipeline/publish.go | 13 ++++++------
3 files changed, 66 insertions(+), 16 deletions(-)
diff --git a/internal/pipeline/climanifest.go b/internal/pipeline/climanifest.go
index 1e8bd9dc..55fc346e 100644
--- a/internal/pipeline/climanifest.go
+++ b/internal/pipeline/climanifest.go
@@ -76,6 +76,25 @@ func WriteCLIManifest(dir string, m CLIManifest) error {
return nil
}
+// findArchivedSpec looks for a spec file archived alongside a generated CLI.
+// generate archives the source spec as spec.json (for JSON inputs) or
+// spec.yaml (for YAML inputs); older runs occasionally used spec.yml. Returns
+// the first match's path and contents, or an empty path with nil error when
+// no archive is present.
+func findArchivedSpec(dir string) (string, []byte, error) {
+ for _, name := range []string{"spec.json", "spec.yaml", "spec.yml"} {
+ path := filepath.Join(dir, name)
+ data, err := os.ReadFile(path)
+ if err == nil {
+ return path, data, nil
+ }
+ if !os.IsNotExist(err) {
+ return "", nil, fmt.Errorf("reading %s: %w", path, err)
+ }
+ }
+ return "", nil, nil
+}
+
// specChecksum computes a SHA-256 checksum of the file at path.
// Returns "sha256:<hex>" on success, or an empty string if the file
// does not exist.
@@ -173,22 +192,15 @@ func WriteManifestForGenerate(p GenerateManifestParams) error {
// Fallback: detect format and checksum from any spec file cached in the output dir.
if m.SpecFormat == "" || m.SpecChecksum == "" {
- for _, name := range []string{"spec.json", "spec.yaml", "spec.yml"} {
- specFile := filepath.Join(p.OutputDir, name)
- data, err := os.ReadFile(specFile)
- if err != nil {
- continue
- }
+ if specFile, data, err := findArchivedSpec(p.OutputDir); err == nil && specFile != "" {
if m.SpecFormat == "" {
m.SpecFormat = detectSpecFormat(data)
}
if m.SpecChecksum == "" {
- cs, err := specChecksum(specFile)
- if err == nil {
+ if cs, err := specChecksum(specFile); err == nil {
m.SpecChecksum = cs
}
}
- break
}
}
diff --git a/internal/pipeline/climanifest_test.go b/internal/pipeline/climanifest_test.go
index 7dd71966..1151e730 100644
--- a/internal/pipeline/climanifest_test.go
+++ b/internal/pipeline/climanifest_test.go
@@ -259,6 +259,45 @@ func TestPublishManifestNormalizesURLDuplicatedInBothFields(t *testing.T) {
assert.Empty(t, got.SpecPath, "URL should not be duplicated in spec_path")
}
+func TestPublishWorkingCLIWritesManifestForYAMLSpec(t *testing.T) {
+ home := setPressTestEnv(t)
+
+ workingDir := filepath.Join(home, "working", "yaml-spec-pp-cli")
+ require.NoError(t, os.MkdirAll(workingDir, 0o755))
+ require.NoError(t, os.WriteFile(
+ filepath.Join(workingDir, "main.go"),
+ []byte("package main\nfunc main() {}"),
+ 0o644,
+ ))
+
+ specContent := []byte("openapi: 3.0.0\ninfo:\n title: YAML Test\n version: 1.0.0\npaths: {}\n")
+ require.NoError(t, os.WriteFile(
+ filepath.Join(workingDir, "spec.yaml"),
+ specContent,
+ 0o644,
+ ))
+
+ state := NewState("yaml-api", workingDir)
+ require.NoError(t, os.MkdirAll(filepath.Dir(state.StatePath()), 0o755))
+ require.NoError(t, state.Save())
+
+ publishDir := filepath.Join(home, "library", "yaml-spec-pp-cli")
+ finalDir, err := PublishWorkingCLI(state, publishDir)
+ require.NoError(t, err)
+
+ data, err := os.ReadFile(filepath.Join(finalDir, CLIManifestFilename))
+ require.NoError(t, err)
+
+ var got CLIManifest
+ require.NoError(t, json.Unmarshal(data, &got))
+
+ assert.Equal(t, "openapi3", got.SpecFormat, "publish must detect format of YAML-archived specs")
+
+ h := sha256.Sum256(specContent)
+ expectedChecksum := "sha256:" + hex.EncodeToString(h[:])
+ assert.Equal(t, expectedChecksum, got.SpecChecksum, "publish must checksum YAML-archived specs")
+}
+
func TestPublishWorkingCLIManifestWithoutSpec(t *testing.T) {
home := setPressTestEnv(t)
diff --git a/internal/pipeline/publish.go b/internal/pipeline/publish.go
index 2509c64a..13125e73 100644
--- a/internal/pipeline/publish.go
+++ b/internal/pipeline/publish.go
@@ -217,14 +217,13 @@ func writeCLIManifestForPublish(state *PipelineState, dir string) error {
}
}
- // Detect spec format and compute checksum from the spec file in the
- // working directory. spec.json only exists when specFlag is --spec;
- // for --docs runs it won't be present and these fields stay empty.
- specFile := filepath.Join(state.EffectiveWorkingDir(), "spec.json")
- if data, err := os.ReadFile(specFile); err == nil {
+ // Detect spec format and compute checksum from the spec file archived
+ // alongside the CLI. generate writes spec.json for JSON inputs and
+ // spec.yaml for YAML inputs; --docs / --plan runs leave no archive and
+ // these fields stay empty.
+ if specFile, data, err := findArchivedSpec(state.EffectiveWorkingDir()); err == nil && specFile != "" {
m.SpecFormat = detectSpecFormat(data)
- checksum, err := specChecksum(specFile)
- if err == nil {
+ if checksum, err := specChecksum(specFile); err == nil {
m.SpecChecksum = checksum
}
← 16ed5a56 feat(cli): printing-press patch — AST-inject PR #218 feature
·
back to Cli Printing Press
·
feat(cli): patch skips AST mutations owned by colliding feat 331809da →