← back to Cli Printing Press
fix(generator): wrapWithProvenance handles non-JSON response bodies (#464)
eeb12ea0c1c1cd18bff2489bab8e003f18a5e767 · 2026-05-01 13:40:41 -0700 · Trevin Chow
For endpoints that return XML, RSS, plain text, or any other non-JSON
content type (e.g., pypi's /rss/updates.xml feed), the previous template
generated a wrapWithProvenance that called json.Marshal on a
json.RawMessage holding the raw bytes. json.Marshal validates embedded
RawMessages, so it errored with "invalid character '<'" the moment the
body started with an XML declaration — causing every --agent invocation
of an XML-returning endpoint to crash before reaching the consumer.
Surfaced by dogfooding pypi (#166): rss recent-updates --agent failed
with that exact error against pypi.org's RSS feed despite the spec
correctly declaring application/xml as the response content type.
Fix: when data isn't valid JSON, embed it as a JSON string so the
envelope marshals cleanly and the raw payload still reaches the
consumer. Valid JSON keeps its previous shape (embedded as the parsed
structure, not stringified).
Also fixes a pre-existing test (TestPRTitleWorkflowAllowsReleasePleaseScope)
left stale by #463 — the test asserted a scopes allow-list that #463
intentionally removed. Updated to accept either shape.
Verified end-to-end:
- regenerated pypi from source spec
- built binary
- rss recent-updates --agent now returns the full RSS XML wrapped as a
JSON string under the {meta, results} envelope
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Files touched
M internal/cli/release_test.goM internal/generator/templates/helpers.go.tmplM testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/helpers.go
Diff
commit eeb12ea0c1c1cd18bff2489bab8e003f18a5e767
Author: Trevin Chow <trevin@trevinchow.com>
Date: Fri May 1 13:40:41 2026 -0700
fix(generator): wrapWithProvenance handles non-JSON response bodies (#464)
For endpoints that return XML, RSS, plain text, or any other non-JSON
content type (e.g., pypi's /rss/updates.xml feed), the previous template
generated a wrapWithProvenance that called json.Marshal on a
json.RawMessage holding the raw bytes. json.Marshal validates embedded
RawMessages, so it errored with "invalid character '<'" the moment the
body started with an XML declaration — causing every --agent invocation
of an XML-returning endpoint to crash before reaching the consumer.
Surfaced by dogfooding pypi (#166): rss recent-updates --agent failed
with that exact error against pypi.org's RSS feed despite the spec
correctly declaring application/xml as the response content type.
Fix: when data isn't valid JSON, embed it as a JSON string so the
envelope marshals cleanly and the raw payload still reaches the
consumer. Valid JSON keeps its previous shape (embedded as the parsed
structure, not stringified).
Also fixes a pre-existing test (TestPRTitleWorkflowAllowsReleasePleaseScope)
left stale by #463 — the test asserted a scopes allow-list that #463
intentionally removed. Updated to accept either shape.
Verified end-to-end:
- regenerated pypi from source spec
- built binary
- rss recent-updates --agent now returns the full RSS XML wrapped as a
JSON string under the {meta, results} envelope
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---
internal/cli/release_test.go | 22 +++++++++++++---------
internal/generator/templates/helpers.go.tmpl | 13 +++++++++++--
.../printing-press-golden/internal/cli/helpers.go | 13 +++++++++++--
3 files changed, 35 insertions(+), 13 deletions(-)
diff --git a/internal/cli/release_test.go b/internal/cli/release_test.go
index 2e2b4cb4..4b6779dd 100644
--- a/internal/cli/release_test.go
+++ b/internal/cli/release_test.go
@@ -146,8 +146,11 @@ func majorVersion(t *testing.T, v string) int {
func TestPRTitleWorkflowAllowsReleasePleaseScope(t *testing.T) {
// release-please uses the target branch as the conventional-commit scope
// for generated release PR titles, e.g. chore(main): release 2.2.0.
- // The PR title workflow must allow that generated title or release PRs
- // cannot merge.
+ // The PR title workflow must accept that scope. Two valid configurations:
+ // - explicit scopes allow-list containing "main"
+ // - no allow-list at all (any scope passes), with requireScope: true
+ // PR #463 removed the allow-list to stop friction with package-name
+ // scopes like `regenmerge`; this test pins both shapes as acceptable.
data, err := os.ReadFile("../../.github/workflows/pr-title.yml")
require.NoError(t, err)
@@ -169,15 +172,16 @@ func TestPRTitleWorkflowAllowsReleasePleaseScope(t *testing.T) {
continue
}
- scopes, ok := step.With["scopes"].(string)
- require.True(t, ok, "semantic pull request action should declare scopes")
-
- allowed := map[string]bool{}
- for scope := range strings.FieldsSeq(scopes) {
- allowed[scope] = true
+ if scopes, ok := step.With["scopes"].(string); ok {
+ allowed := map[string]bool{}
+ for scope := range strings.FieldsSeq(scopes) {
+ allowed[scope] = true
+ }
+ assert.True(t, allowed["main"], "scope allow-list must include 'main' for release-please PR titles")
+ return
}
- assert.True(t, allowed["main"], "release-please PR titles use main as the scope")
+ // No allow-list — any scope is accepted, including release-please's "main".
return
}
diff --git a/internal/generator/templates/helpers.go.tmpl b/internal/generator/templates/helpers.go.tmpl
index 02b91c31..68decd3e 100644
--- a/internal/generator/templates/helpers.go.tmpl
+++ b/internal/generator/templates/helpers.go.tmpl
@@ -1250,7 +1250,12 @@ func printProvenance(cmd *cobra.Command, count int, prov DataProvenance) {
fmt.Fprintf(cmd.ErrOrStderr(), "%s%d results (cached, synced %s)\n", prefix, count, age)
}
-// wrapWithProvenance wraps JSON data in a provenance envelope: {"results": ..., "meta": {...}}.
+// wrapWithProvenance wraps response data in a provenance envelope:
+// {"results": ..., "meta": {...}}. When data is valid JSON, it embeds as
+// the parsed shape; when data is non-JSON (e.g., XML/RSS responses, plain
+// text), it embeds as a JSON string so json.Marshal doesn't choke on
+// "invalid character '<'" while still passing the raw payload through to
+// the consumer.
func wrapWithProvenance(data json.RawMessage, prov DataProvenance) (json.RawMessage, error) {
meta := map[string]any{"source": prov.Source}
if prov.SyncedAt != nil {
@@ -1265,8 +1270,12 @@ func wrapWithProvenance(data json.RawMessage, prov DataProvenance) (json.RawMess
if prov.Freshness != nil {
meta["freshness"] = prov.Freshness
}
+ var results any = json.RawMessage(data)
+ if !json.Valid(data) {
+ results = string(data)
+ }
envelope := map[string]any{
- "results": json.RawMessage(data),
+ "results": results,
"meta": meta,
}
return json.Marshal(envelope)
diff --git a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/helpers.go b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/helpers.go
index 119e1003..8913e4f0 100644
--- a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/helpers.go
+++ b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/helpers.go
@@ -1137,7 +1137,12 @@ func printProvenance(cmd *cobra.Command, count int, prov DataProvenance) {
fmt.Fprintf(cmd.ErrOrStderr(), "%s%d results (cached, synced %s)\n", prefix, count, age)
}
-// wrapWithProvenance wraps JSON data in a provenance envelope: {"results": ..., "meta": {...}}.
+// wrapWithProvenance wraps response data in a provenance envelope:
+// {"results": ..., "meta": {...}}. When data is valid JSON, it embeds as
+// the parsed shape; when data is non-JSON (e.g., XML/RSS responses, plain
+// text), it embeds as a JSON string so json.Marshal doesn't choke on
+// "invalid character '<'" while still passing the raw payload through to
+// the consumer.
func wrapWithProvenance(data json.RawMessage, prov DataProvenance) (json.RawMessage, error) {
meta := map[string]any{"source": prov.Source}
if prov.SyncedAt != nil {
@@ -1152,8 +1157,12 @@ func wrapWithProvenance(data json.RawMessage, prov DataProvenance) (json.RawMess
if prov.Freshness != nil {
meta["freshness"] = prov.Freshness
}
+ var results any = json.RawMessage(data)
+ if !json.Valid(data) {
+ results = string(data)
+ }
envelope := map[string]any{
- "results": json.RawMessage(data),
+ "results": results,
"meta": meta,
}
return json.Marshal(envelope)
← 9bc8276e ci: drop the PR-title scope allow-list, keep requireScope (#
·
back to Cli Printing Press
·
fix(openapi): preserve params on single-endpoint specs (#465 45a7722b →